1 /*
2 * kalarmapp.cpp - the KAlarm application object
3 * Program: kalarm
4 * SPDX-FileCopyrightText: 2001-2021 David Jarvie <djarvie@kde.org>
5 *
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9 #include "kalarmapp.h"
10
11 #include "kalarm.h"
12 #include "commandoptions.h"
13 #include "dbushandler.h"
14 #include "displaycalendar.h"
15 #include "editdlgtypes.h"
16 #include "functions.h"
17 #include "mainwindow.h"
18 #include "messagewindow.h"
19 #include "messagenotification.h"
20 #include "prefdlg.h"
21 #include "resourcescalendar.h"
22 #include "startdaytimer.h"
23 #include "traywindow.h"
24 #include "resources/datamodel.h"
25 #include "resources/resources.h"
26 #include "resources/eventmodel.h"
27 #include "lib/desktop.h"
28 #include "lib/messagebox.h"
29 #include "lib/shellprocess.h"
30 #include "notifications_interface.h" // DBUS-generated
31 #include "dbusproperties.h" // DBUS-generated
32 #include "kalarm_debug.h"
33 #include <kcoreaddons_version.h>
34 #if KCOREADDONS_VERSION < QT_VERSION_CHECK(6, 0, 0)
35 #include "migratekde4files.h"
36 #endif
37 #include <KAlarmCal/DateTime>
38 #include <KAlarmCal/KARecurrence>
39
40 #include <KLocalizedString>
41 #include <KConfig>
42 #include <KConfigGui>
43 #include <KAboutData>
44 #include <KSharedConfig>
45 #include <KStandardGuiItem>
46 #include <netwm.h>
47 #include <KShell>
48
49 #include <QObject>
50 #include <QTimer>
51 #include <QFile>
52 #include <QTextStream>
53 #include <QTemporaryFile>
54 #include <QStandardPaths>
55 #include <QSystemTrayIcon>
56 #include <QCommandLineParser>
57
58 #include <stdlib.h>
59 #include <ctype.h>
60 #include <iostream>
61 #include <climits>
62
63 namespace
64 {
65 const int RESOURCES_TIMEOUT = 30; // timeout (seconds) for resources to be populated
66
67 const char FDO_NOTIFICATIONS_SERVICE[] = "org.freedesktop.Notifications";
68 const char FDO_NOTIFICATIONS_PATH[] = "/org/freedesktop/Notifications";
69
70 /******************************************************************************
71 * Find the maximum number of seconds late which a late-cancel alarm is allowed
72 * to be. This is calculated as the late cancel interval, plus a few seconds
73 * leeway to cater for any timing irregularities.
74 */
maxLateness(int lateCancel)75 inline int maxLateness(int lateCancel)
76 {
77 static const int LATENESS_LEEWAY = 5;
78 int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0;
79 return LATENESS_LEEWAY + lc;
80 }
81
mainWidget()82 QWidget* mainWidget()
83 {
84 return MainWindow::mainMainWindow();
85 }
86 }
87
88
89 KAlarmApp* KAlarmApp::mInstance = nullptr;
90 int KAlarmApp::mActiveCount = 0;
91 int KAlarmApp::mFatalError = 0;
92 QString KAlarmApp::mFatalMessage;
93
94
95 /******************************************************************************
96 * Construct the application.
97 */
KAlarmApp(int & argc,char ** argv)98 KAlarmApp::KAlarmApp(int& argc, char** argv)
99 : QApplication(argc, argv)
100 , mDBusHandler(new DBusHandler())
101 {
102 qCDebug(KALARM_LOG) << "KAlarmApp:";
103 }
104
105 /******************************************************************************
106 */
~KAlarmApp()107 KAlarmApp::~KAlarmApp()
108 {
109 while (!mCommandProcesses.isEmpty())
110 {
111 ProcData* pd = mCommandProcesses.at(0);
112 mCommandProcesses.pop_front();
113 delete pd;
114 }
115 ResourcesCalendar::terminate();
116 DisplayCalendar::terminate();
117 DataModel::terminate();
118 delete mDBusHandler;
119 }
120
121 /******************************************************************************
122 * Return the one and only KAlarmApp instance.
123 * If it doesn't already exist, it is created first.
124 */
create(int & argc,char ** argv)125 KAlarmApp* KAlarmApp::create(int& argc, char** argv)
126 {
127 if (!mInstance)
128 {
129 mInstance = new KAlarmApp(argc, argv);
130
131 if (mFatalError)
132 mInstance->quitFatal();
133 }
134 return mInstance;
135 }
136
137 /******************************************************************************
138 * Perform initialisations which may require KAboutData to have been set up.
139 */
initialise()140 void KAlarmApp::initialise()
141 {
142 #if KCOREADDONS_VERSION < QT_VERSION_CHECK(6, 0, 0)
143 // Migrate config and data files from KDE4 locations.
144 MigrateKde4Files migrate;
145 migrate.migrate();
146 #endif
147
148 #ifndef NDEBUG
149 KAlarm::setTestModeConditions();
150 #endif
151
152 setQuitOnLastWindowClosed(false);
153 Preferences::self(); // read KAlarm configuration
154 if (!Preferences::noAutoStart())
155 {
156 // Strip out any "OnlyShowIn=KDE" list from kalarm.autostart.desktop
157 Preferences::setNoAutoStart(false);
158 // Enable kalarm.autostart.desktop to start KAlarm
159 Preferences::setAutoStart(true);
160 Preferences::self()->save();
161 }
162 Preferences::connect(&Preferences::startOfDayChanged, this, &KAlarmApp::changeStartOfDay);
163 Preferences::connect(&Preferences::workTimeChanged, this, &KAlarmApp::slotWorkTimeChanged);
164 Preferences::connect(&Preferences::holidaysChanged, this, &KAlarmApp::slotHolidaysChanged);
165 Preferences::connect(&Preferences::feb29TypeChanged, this, &KAlarmApp::slotFeb29TypeChanged);
166 Preferences::connect(&Preferences::showInSystemTrayChanged, this, &KAlarmApp::slotShowInSystemTrayChanged);
167 Preferences::connect(&Preferences::archivedKeepDaysChanged, this, &KAlarmApp::setArchivePurgeDays);
168 Preferences::connect(&Preferences::messageFontChanged, this, &KAlarmApp::slotMessageFontChanged);
169 slotFeb29TypeChanged(Preferences::defaultFeb29Type());
170
171 KAEvent::setStartOfDay(Preferences::startOfDay());
172 KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd());
173 KAEvent::setHolidays(Preferences::holidays());
174 KAEvent::setDefaultFont(Preferences::messageFont());
175
176 // Check if KOrganizer is installed
177 const QString korg = QStringLiteral("korganizer");
178 mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty();
179 if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; }
180 // Check if the window manager can't handle keyboard focus transfer between windows
181 mWindowFocusBroken = (Desktop::currentIdentity() == Desktop::Unity);
182 if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; }
183
184 Resources* resources = Resources::instance();
185 connect(resources, &Resources::resourceAdded,
186 this, &KAlarmApp::slotResourceAdded);
187 connect(resources, &Resources::resourcePopulated,
188 this, &KAlarmApp::slotResourcePopulated);
189 connect(resources, &Resources::resourcePopulated,
190 this, &KAlarmApp::purgeNewArchivedDefault);
191 connect(resources, &Resources::resourcesCreated,
192 this, &KAlarmApp::slotResourcesCreated);
193 connect(resources, &Resources::resourcesPopulated,
194 this, &KAlarmApp::processQueue);
195
196 initialiseTimerResources(); // initialise calendars and alarm timer
197
198 KConfigGroup config(KSharedConfig::openConfig(), "General");
199 mNoSystemTray = config.readEntry("NoSystemTray", false);
200 mOldShowInSystemTray = wantShowInSystemTray();
201 DateTime::setStartOfDay(Preferences::startOfDay());
202 mPrefsArchivedColour = Preferences::archivedColour();
203
204 // Get notified when the Freedesktop notifications properties have changed.
205 QDBusConnection conn = QDBusConnection::sessionBus();
206 if (conn.interface()->isServiceRegistered(QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE)))
207 {
208 OrgFreedesktopDBusPropertiesInterface* piface = new OrgFreedesktopDBusPropertiesInterface(
209 QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE),
210 QString::fromLatin1(FDO_NOTIFICATIONS_PATH),
211 conn, this);
212 connect(piface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
213 this, &KAlarmApp::slotFDOPropertiesChanged);
214 OrgFreedesktopNotificationsInterface niface(
215 QString::fromLatin1(FDO_NOTIFICATIONS_SERVICE),
216 QString::fromLatin1(FDO_NOTIFICATIONS_PATH),
217 conn);
218 mNotificationsInhibited = niface.inhibited();
219 }
220 }
221
222 /******************************************************************************
223 * Initialise or reinitialise things which are tidied up/closed by quitIf().
224 * Reinitialisation can be necessary if session restoration finds nothing to
225 * restore and starts quitting the application, but KAlarm then starts up again
226 * before the application has exited.
227 * Reply = true if calendars were initialised successfully,
228 * false if they were already initialised, or if initialisation failed.
229 */
initialiseTimerResources()230 bool KAlarmApp::initialiseTimerResources()
231 {
232 if (!mAlarmTimer)
233 {
234 mAlarmTimer = new QTimer(this);
235 mAlarmTimer->setSingleShot(true);
236 connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm);
237 }
238 if (!ResourcesCalendar::instance())
239 {
240 qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars";
241 Desktop::setMainWindowFunc(&mainWidget);
242 // First, initialise calendar resources, which need to be ready to
243 // receive signals when resources initialise.
244 ResourcesCalendar::initialise(KALARM_NAME, KALARM_VERSION);
245 connect(ResourcesCalendar::instance(), &ResourcesCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm);
246 connect(ResourcesCalendar::instance(), &ResourcesCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded);
247 DisplayCalendar::initialise();
248 // Finally, initialise the resources which generate signals as they initialise.
249 DataModel::initialise();
250 return true;
251 }
252 return false;
253 }
254
255 /******************************************************************************
256 * Restore the saved session if required.
257 */
restoreSession()258 bool KAlarmApp::restoreSession()
259 {
260 if (!isSessionRestored())
261 return false;
262 if (mFatalError)
263 {
264 quitFatal();
265 return false;
266 }
267
268 // Process is being restored by session management.
269 qCDebug(KALARM_LOG) << "KAlarmApp::restoreSession: Restoring";
270 ++mActiveCount;
271 // Create the session config object now.
272 // This is necessary since if initCheck() below causes calendars to be updated,
273 // the session config created after that points to an invalid file, resulting
274 // in no windows being restored followed by a later crash.
275 KConfigGui::sessionConfig();
276
277 // When KAlarm is session restored, automatically set start-at-login to true.
278 Preferences::self()->load();
279 Preferences::setAutoStart(true);
280 Preferences::setNoAutoStart(false);
281 Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression
282 Preferences::self()->save();
283
284 if (!initCheck(true)) // open the calendar file (needed for main windows), don't process queue yet
285 {
286 --mActiveCount;
287 quitIf(1, true); // error opening the main calendar - quit
288 return false;
289 }
290 MainWindow* trayParent = nullptr;
291 for (int i = 1; KMainWindow::canBeRestored(i); ++i)
292 {
293 const QString type = KMainWindow::classNameOfToplevel(i);
294 if (type == QLatin1String("MainWindow"))
295 {
296 MainWindow* win = MainWindow::create(true);
297 win->restore(i, false);
298 if (win->isHiddenTrayParent())
299 trayParent = win;
300 else
301 win->show();
302 }
303 else if (type == QLatin1String("MessageWindow"))
304 {
305 auto win = new MessageWindow;
306 win->restore(i, false);
307 if (win->isValid())
308 {
309 if (Resources::allCreated() && !mNotificationsInhibited)
310 win->display();
311 else
312 mRestoredWindows += win;
313 }
314 else
315 delete win;
316 }
317 }
318
319 MessageNotification::sessionRestore();
320
321 // Try to display the system tray icon if it is configured to be shown
322 if (trayParent || wantShowInSystemTray())
323 {
324 if (!MainWindow::count())
325 qCWarning(KALARM_LOG) << "KAlarmApp::restoreSession: no main window to be restored!?";
326 else
327 {
328 displayTrayIcon(true, trayParent);
329 // Occasionally for no obvious reason, the main main window is
330 // shown when it should be hidden, so hide it just to be sure.
331 if (trayParent)
332 trayParent->hide();
333 }
334 }
335
336 --mActiveCount;
337 if (quitIf(0)) // quit if no windows are open
338 return false; // quitIf() can sometimes return, despite calling exit()
339
340 startProcessQueue(); // start processing the execution queue
341 return true;
342 }
343
344 /******************************************************************************
345 * If resources have been created and notifications are not inhibited,
346 * show message windows restored at startup which are waiting to be displayed,
347 * and redisplay alarms showing when the program crashed or was killed.
348 */
showRestoredWindows()349 void KAlarmApp::showRestoredWindows()
350 {
351 if (!mNotificationsInhibited && Resources::allCreated())
352 {
353 if (!mRestoredWindows.isEmpty())
354 {
355 // Display message windows restored at startup.
356 for (MessageWindow* win : std::as_const(mRestoredWindows))
357 win->display();
358 mRestoredWindows.clear();
359 }
360 if (mRedisplayAlarms)
361 {
362 // Display alarms which were showing when the program crashed or was killed.
363 mRedisplayAlarms = false;
364 MessageDisplay::redisplayAlarms();
365 }
366 }
367 }
368
369 /******************************************************************************
370 * Called to start a new instance of the unique QApplication.
371 * Reply: exit code (>= 0), or -1 to continue execution.
372 * If exit code >= 0, 'outputText' holds text to output before terminating.
373 */
activateInstance(const QStringList & args,const QString & workingDirectory,QString * outputText)374 int KAlarmApp::activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText)
375 {
376 Q_UNUSED(workingDirectory)
377 qCDebug(KALARM_LOG) << "KAlarmApp::activateInstance" << args;
378 if (outputText)
379 outputText->clear();
380 if (mFatalError)
381 {
382 Q_EMIT setExitValue(1);
383 quitFatal();
384 return 1;
385 }
386
387 // The D-Bus call to activate a subsequent instance of KAlarm may not supply
388 // any arguments, but we need one.
389 if (!args.isEmpty() && mActivateArg0.isEmpty())
390 mActivateArg0 = args[0];
391 QStringList fixedArgs(args);
392 if (args.isEmpty() && !mActivateArg0.isEmpty())
393 fixedArgs << mActivateArg0;
394
395 // Parse and interpret command line arguments.
396 QCommandLineParser parser;
397 KAboutData::applicationData().setupCommandLine(&parser);
398 parser.setApplicationDescription(QApplication::applicationDisplayName());
399 auto options = new CommandOptions;
400 const QStringList nonexecArgs = options->setOptions(&parser, fixedArgs);
401 options->parse();
402 KAboutData::applicationData().processCommandLine(&parser);
403
404 ++mActiveCount;
405 int exitCode = 0; // default = success
406 static bool firstInstance = true;
407 bool dontRedisplay = false;
408 CommandOptions::Command command = CommandOptions::NONE;
409 const bool processOptions = (!firstInstance || !isSessionRestored());
410 if (processOptions)
411 {
412 options->process();
413 #ifndef NDEBUG
414 if (options->simulationTime().isValid())
415 KAlarm::setSimulatedSystemTime(options->simulationTime());
416 #endif
417 command = options->command();
418 if (options->disableAll())
419 setAlarmsEnabled(false); // disable alarm monitoring
420
421 // Handle options which exit with a terminal message, before
422 // making the application a unique application, since a
423 // unique application won't output to the terminal if another
424 // instance is already running.
425 switch (command)
426 {
427 case CommandOptions::CMD_ERROR:
428 Q_EMIT setExitValue(1);
429 if (outputText)
430 {
431 // Instance was activated from main().
432 *outputText = options->outputText();
433 delete options;
434 return 1;
435 }
436 // Instance was activated by DBus.
437 std::cerr << qPrintable(options->outputText()) << std::endl;;
438 mReadOnly = true; // don't need write access to calendars
439 exitCode = 1;
440 break;
441 default:
442 break;
443 }
444 }
445
446 if (processOptions)
447 {
448 switch (command)
449 {
450 case CommandOptions::TRIGGER_EVENT:
451 case CommandOptions::CANCEL_EVENT:
452 {
453 // Display or delete the event with the specified event ID
454 auto action = static_cast<QueuedAction>(int((command == CommandOptions::TRIGGER_EVENT) ? QueuedAction::Trigger : QueuedAction::Cancel)
455 | int(QueuedAction::Exit));
456 // Open the calendar, don't start processing execution queue yet,
457 // and wait for the calendar resources to be populated.
458 if (!initCheck(true))
459 exitCode = 1;
460 else
461 {
462 mCommandOption = options->commandName();
463 // Get the resource ID string and event UID. Note that if
464 // resources have not been created yet, the numeric
465 // resource ID can't yet be looked up.
466 if (options->resourceId().isEmpty())
467 action = static_cast<QueuedAction>((int)action | int(QueuedAction::FindId));
468 mActionQueue.enqueue(ActionQEntry(action, EventId(options->eventId()), options->resourceId()));
469 startProcessQueue(true); // start processing the execution queue
470 dontRedisplay = true;
471 }
472 break;
473 }
474 case CommandOptions::LIST:
475 // Output a list of scheduled alarms to stdout.
476 // Open the calendar, don't start processing execution queue yet,
477 // and wait for all calendar resources to be populated.
478 mReadOnly = true; // don't need write access to calendars
479 if (firstInstance)
480 mAlarmsEnabled = false; // prevent alarms being processed if no other instance is running
481 if (!initCheck(true))
482 exitCode = 1;
483 else
484 {
485 const auto action = static_cast<QueuedAction>(int(QueuedAction::List) | int(QueuedAction::Exit));
486 mActionQueue.enqueue(ActionQEntry(action, EventId()));
487 startProcessQueue(true); // start processing the execution queue
488 dontRedisplay = true;
489 }
490 break;
491
492 case CommandOptions::EDIT:
493 // Edit a specified existing alarm.
494 // Open the calendar and wait for the calendar resources to be populated.
495 if (!initCheck(false))
496 exitCode = 1;
497 else
498 {
499 mCommandOption = options->commandName();
500 if (firstInstance)
501 mEditingCmdLineAlarm = 0x10; // want to redisplay alarms if successful
502 // Get the resource ID string and event UID. Note that if
503 // resources have not been created yet, the numeric
504 // resource ID can't yet be looked up.
505 mActionQueue.enqueue(ActionQEntry(QueuedAction::Edit, EventId(options->eventId()), options->resourceId()));
506 startProcessQueue(true); // start processing the execution queue
507 dontRedisplay = true;
508 }
509 break;
510
511 case CommandOptions::EDIT_NEW:
512 {
513 // Edit a new alarm, and optionally preset selected values
514 if (!initCheck())
515 exitCode = 1;
516 else
517 {
518 EditAlarmDlg* editDlg = EditAlarmDlg::create(false, options->editType(), MainWindow::mainMainWindow());
519 if (!editDlg)
520 {
521 exitCode = 1;
522 break;
523 }
524 editDlg->setName(options->name());
525 if (options->alarmTime().isValid())
526 editDlg->setTime(options->alarmTime());
527 if (options->recurrence())
528 editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount());
529 else if (options->flags() & KAEvent::REPEAT_AT_LOGIN)
530 editDlg->setRepeatAtLogin();
531 editDlg->setAction(options->editAction(), AlarmText(options->text()));
532 if (options->lateCancel())
533 editDlg->setLateCancel(options->lateCancel());
534 if (options->flags() & KAEvent::COPY_KORGANIZER)
535 editDlg->setShowInKOrganizer(true);
536 switch (options->editType())
537 {
538 case EditAlarmDlg::DISPLAY:
539 {
540 // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY
541 auto dlg = qobject_cast<EditDisplayAlarmDlg*>(editDlg);
542 if (options->fgColour().isValid())
543 dlg->setFgColour(options->fgColour());
544 if (options->bgColour().isValid())
545 dlg->setBgColour(options->bgColour());
546 if (!options->audioFile().isEmpty()
547 || options->flags() & (KAEvent::BEEP | KAEvent::SPEAK))
548 {
549 const KAEvent::Flags flags = options->flags();
550 const Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep
551 : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak
552 : Preferences::Sound_File;
553 dlg->setAudio(type, options->audioFile(), options->audioVolume(), (flags & KAEvent::REPEAT_SOUND ? 0 : -1));
554 }
555 if (options->reminderMinutes())
556 dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE));
557 if (options->flags() & KAEvent::NOTIFY)
558 dlg->setNotify(true);
559 if (options->flags() & KAEvent::CONFIRM_ACK)
560 dlg->setConfirmAck(true);
561 if (options->flags() & KAEvent::AUTO_CLOSE)
562 dlg->setAutoClose(true);
563 break;
564 }
565 case EditAlarmDlg::COMMAND:
566 break;
567 case EditAlarmDlg::EMAIL:
568 {
569 // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL
570 auto dlg = qobject_cast<EditEmailAlarmDlg*>(editDlg);
571 if (options->fromID()
572 || !options->addressees().isEmpty()
573 || !options->subject().isEmpty()
574 || !options->attachments().isEmpty())
575 dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments());
576 if (options->flags() & KAEvent::EMAIL_BCC)
577 dlg->setBcc(true);
578 break;
579 }
580 case EditAlarmDlg::AUDIO:
581 {
582 // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO
583 auto dlg = qobject_cast<EditAudioAlarmDlg*>(editDlg);
584 if (!options->audioFile().isEmpty() || options->audioVolume() >= 0)
585 dlg->setAudio(options->audioFile(), options->audioVolume());
586 break;
587 }
588 case EditAlarmDlg::NO_TYPE:
589 break;
590 }
591
592 // Execute the edit dialogue. Note that if no other instance of KAlarm is
593 // running, this new instance will not exit after the dialogue is closed.
594 // This is deliberate, since exiting would mean that KAlarm wouldn't
595 // trigger the new alarm.
596 KAlarm::execNewAlarmDlg(editDlg);
597
598 createOnlyMainWindow(); // prevent the application from quitting
599 }
600 break;
601 }
602 case CommandOptions::EDIT_NEW_PRESET:
603 // Edit a new alarm, preset with a template
604 if (!initCheck())
605 exitCode = 1;
606 else
607 {
608 // Execute the edit dialogue. Note that if no other instance of KAlarm is
609 // running, this new instance will not exit after the dialogue is closed.
610 // This is deliberate, since exiting would mean that KAlarm wouldn't
611 // trigger the new alarm.
612 KAlarm::editNewAlarm(options->name());
613
614 createOnlyMainWindow(); // prevent the application from quitting
615 }
616 break;
617
618 case CommandOptions::NEW:
619 // Display a message or file, execute a command, or send an email
620 setResourcesTimeout(); // set timeout for resource initialisation
621 if (!initCheck())
622 exitCode = 1;
623 else
624 {
625 QueuedAction flags = QueuedAction::CmdLine;
626 if (!MainWindow::count())
627 flags = static_cast<QueuedAction>(int(flags) + int(QueuedAction::ErrorExit));
628 if (!scheduleEvent(flags,
629 options->editAction(), options->name(), options->text(),
630 options->alarmTime(), options->lateCancel(), options->flags(),
631 options->bgColour(), options->fgColour(), QFont(),
632 options->audioFile(), options->audioVolume(),
633 options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()),
634 options->subRepeatInterval(), options->subRepeatCount(),
635 options->fromID(), options->addressees(),
636 options->subject(), options->attachments()))
637 exitCode = 1;
638 else
639 createOnlyMainWindow(); // prevent the application from quitting
640 }
641 break;
642
643 case CommandOptions::TRAY:
644 // Display only the system tray icon
645 if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable())
646 {
647 if (!initCheck()) // open the calendar, start processing execution queue
648 exitCode = 1;
649 else
650 {
651 if (!displayTrayIcon(true))
652 exitCode = 1;
653 }
654 break;
655 }
656 Q_FALLTHROUGH(); // fall through to NONE
657 case CommandOptions::NONE:
658 // No arguments - run interactively & display the main window
659 #ifndef NDEBUG
660 if (options->simulationTime().isValid() && !firstInstance)
661 break; // simulating time: don't open main window if already running
662 #endif
663 if (!initCheck())
664 exitCode = 1;
665 else
666 {
667 if (mTrayWindow && mTrayWindow->assocMainWindow() && !mTrayWindow->assocMainWindow()->isVisible())
668 mTrayWindow->showAssocMainWindow();
669 else
670 {
671 MainWindow* win = MainWindow::create();
672 if (command == CommandOptions::TRAY)
673 win->setWindowState(win->windowState() | Qt::WindowMinimized);
674 win->show();
675 }
676 }
677 break;
678 default:
679 break;
680 }
681 }
682 if (options != CommandOptions::firstInstance())
683 delete options;
684
685 // If this is the first time through, redisplay any alarm message windows
686 // from last time.
687 if (firstInstance && !dontRedisplay && !exitCode)
688 {
689 /* First time through, so redisplay alarm message windows from last time.
690 * But it is possible for session restoration in some circumstances to
691 * not create any windows, in which case the alarm calendars will have
692 * been deleted - if so, don't try to do anything. (This has been known
693 * to happen under the Xfce desktop.)
694 */
695 if (ResourcesCalendar::instance())
696 {
697 mRedisplayAlarms = true;
698 showRestoredWindows();
699 }
700 }
701
702 --mActiveCount;
703 firstInstance = false;
704
705 // Quit the application if this was the last/only running "instance" of the program.
706 // Executing 'return' doesn't work very well since the program continues to
707 // run if no windows were created.
708 if (quitIf(exitCode >= 0 ? exitCode : 0))
709 return exitCode; // exit this application instance
710
711 return -1; // continue executing the application instance
712 }
713
714 /******************************************************************************
715 * Create a minimised main window if none already exists.
716 * This prevents the application from quitting.
717 */
createOnlyMainWindow()718 void KAlarmApp::createOnlyMainWindow()
719 {
720 if (!MainWindow::count())
721 {
722 if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable())
723 {
724 if (displayTrayIcon(true))
725 return;
726 }
727 MainWindow* win = MainWindow::create();
728 win->setWindowState(Qt::WindowMinimized);
729 win->show();
730 }
731 }
732
733 /******************************************************************************
734 * Quit the program, optionally only if there are no more "instances" running.
735 * Reply = true if program exited.
736 */
quitIf(int exitCode,bool force)737 bool KAlarmApp::quitIf(int exitCode, bool force)
738 {
739 if (force)
740 {
741 // Quit regardless, except for message windows
742 mQuitting = true;
743 MainWindow::closeAll();
744 mQuitting = false;
745 displayTrayIcon(false);
746 if (MessageDisplay::instanceCount(true)) // ignore always-hidden displays (e.g. audio alarms)
747 return false;
748 }
749 else if (mQuitting)
750 return false; // MainWindow::closeAll() causes quitIf() to be called again
751 else
752 {
753 // Quit only if there are no more "instances" running
754 mPendingQuit = false;
755 if (mActiveCount > 0 || MessageDisplay::instanceCount(true)) // ignore always-hidden displays (e.g. audio alarms)
756 return false;
757 const int mwcount = MainWindow::count();
758 MainWindow* mw = mwcount ? MainWindow::firstWindow() : nullptr;
759 if (mwcount > 1 || (mwcount && (!mw->isHidden() || !mw->isTrayParent())))
760 return false;
761 // There are no windows left except perhaps a main window which is a hidden
762 // tray icon parent, or an always-hidden message window.
763 if (mTrayWindow)
764 {
765 // There is a system tray icon.
766 // Don't exit unless the system tray doesn't seem to exist.
767 if (checkSystemTray())
768 return false;
769 }
770 if (!mActionQueue.isEmpty() || !mCommandProcesses.isEmpty())
771 {
772 // Don't quit yet if there are outstanding actions on the execution queue
773 mPendingQuit = true;
774 mPendingQuitCode = exitCode;
775 return false;
776 }
777 }
778
779 // This was the last/only running "instance" of the program, so exit completely.
780 // NOTE: Everything which is terminated/deleted here must where applicable
781 // be initialised in the initialiseTimerResources() method, in case
782 // KAlarm is started again before application exit completes!
783 qCDebug(KALARM_LOG) << "KAlarmApp::quitIf:" << exitCode << ": quitting";
784 MessageDisplay::stopAudio(true);
785 if (mCancelRtcWake)
786 {
787 KAlarm::setRtcWakeTime(0, nullptr);
788 KAlarm::deleteRtcWakeConfig();
789 }
790 delete mAlarmTimer; // prevent checking for alarms after deleting calendars
791 mAlarmTimer = nullptr;
792 mInitialised = false; // prevent processQueue() from running
793 ResourcesCalendar::terminate();
794 DisplayCalendar::terminate();
795 DataModel::terminate();
796 Q_EMIT setExitValue(exitCode);
797 exit(exitCode);
798 return true; // sometimes we actually get to here, despite calling exit()
799 }
800
801 /******************************************************************************
802 * Called when the Quit menu item is selected.
803 * Closes the system tray window and all main windows, but does not exit the
804 * program if other windows are still open.
805 */
doQuit(QWidget * parent)806 void KAlarmApp::doQuit(QWidget* parent)
807 {
808 qCDebug(KALARM_LOG) << "KAlarmApp::doQuit";
809 if (KAMessageBox::warningCancelContinue(parent,
810 i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."),
811 QString(), KStandardGuiItem::quit(),
812 KStandardGuiItem::cancel(), Preferences::QUIT_WARN
813 ) != KMessageBox::Continue)
814 return;
815 if (!KAlarm::checkRtcWakeConfig(true).isEmpty())
816 {
817 // A wake-on-suspend alarm is set
818 if (KAMessageBox::warningCancelContinue(parent,
819 i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."),
820 QString(), KStandardGuiItem::quit()
821 ) != KMessageBox::Continue)
822 return;
823 mCancelRtcWake = true;
824 }
825 if (!Preferences::autoStart())
826 {
827 int option = KMessageBox::No;
828 if (!Preferences::autoStartChangedByUser())
829 {
830 option = KAMessageBox::questionYesNoCancel(parent,
831 xi18nc("@info", "Do you want to start KAlarm at login?<nl/>"
832 "(Note that alarms will be disabled if KAlarm is not started.)"),
833 QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(),
834 KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START);
835 }
836 switch (option)
837 {
838 case KMessageBox::Yes:
839 Preferences::setAutoStart(true);
840 Preferences::setNoAutoStart(false);
841 break;
842 case KMessageBox::No:
843 Preferences::setNoAutoStart(true);
844 break;
845 case KMessageBox::Cancel:
846 default:
847 return;
848 }
849 Preferences::self()->save();
850 }
851 quitIf(0, true);
852 }
853
854 /******************************************************************************
855 * Display an error message for a fatal error. Prevent further actions since
856 * the program state is unsafe.
857 */
displayFatalError(const QString & message)858 void KAlarmApp::displayFatalError(const QString& message)
859 {
860 if (!mFatalError)
861 {
862 mFatalError = 1;
863 mFatalMessage = message;
864 if (mInstance)
865 QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
866 }
867 }
868
869 /******************************************************************************
870 * Quit the program, once the fatal error message has been acknowledged.
871 */
quitFatal()872 void KAlarmApp::quitFatal()
873 {
874 switch (mFatalError)
875 {
876 case 0:
877 case 2:
878 return;
879 case 1:
880 mFatalError = 2;
881 KMessageBox::error(nullptr, mFatalMessage); // this is an application modal window
882 mFatalError = 3;
883 Q_FALLTHROUGH(); // fall through to '3'
884 case 3:
885 if (mInstance)
886 mInstance->quitIf(1, true);
887 break;
888 }
889 QTimer::singleShot(1000, this, &KAlarmApp::quitFatal); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
890 }
891
892 /******************************************************************************
893 * Called by the alarm timer when the next alarm is due.
894 * Also called when the execution queue has finished processing to check for the
895 * next alarm.
896 */
checkNextDueAlarm()897 void KAlarmApp::checkNextDueAlarm()
898 {
899 if (!mAlarmsEnabled)
900 return;
901 // Find the first alarm due
902 KADateTime nextDt;
903 const KAEvent nextEvent = ResourcesCalendar::earliestAlarm(nextDt, mNotificationsInhibited);
904 if (!nextEvent.isValid())
905 return; // there are no alarms pending
906 const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec());
907 qint64 interval = now.msecsTo(nextDt);
908 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;
909 if (interval <= 0)
910 {
911 // Queue the alarm
912 queueAlarmId(nextEvent);
913 qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent.id() << ": due now";
914 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
915 }
916 else
917 {
918 // No alarm is due yet, so set timer to wake us when it's due.
919 // Check for integer overflow before setting timer.
920 #ifndef HIBERNATION_SIGNAL
921 /* TODO: Use hibernation wakeup signal:
922 * #include <Solid/Power>
923 * connect(Solid::Power::self(), &Solid::Power::resumeFromSuspend, ...)
924 * (or resumingFromSuspend?)
925 * to be notified when wakeup from hibernation occurs. But can't use it
926 * unless we know that this notification is supported by the system!
927 */
928 /* Re-evaluate the next alarm time every minute, in case the
929 * system clock jumps. The most common case when the clock jumps
930 * is when a laptop wakes from hibernation. If timers were left to
931 * run, they would trigger late by the length of time the system
932 * was asleep.
933 */
934 if (interval > 60000) // 1 minute
935 interval = 60000;
936 #endif
937 ++interval; // ensure we don't trigger just before the minute boundary
938 if (interval > INT_MAX)
939 interval = INT_MAX;
940 qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent.id() << "wait" << interval/1000 << "seconds";
941 mAlarmTimer->start(static_cast<int>(interval));
942 }
943 }
944
945 /******************************************************************************
946 * Called by the alarm timer when the next alarm is due.
947 * Also called when the execution queue has finished processing to check for the
948 * next alarm.
949 */
queueAlarmId(const KAEvent & event)950 void KAlarmApp::queueAlarmId(const KAEvent& event)
951 {
952 const EventId id(event);
953 for (const ActionQEntry& entry : std::as_const(mActionQueue))
954 {
955 if (entry.action == QueuedAction::Handle && entry.eventId == id)
956 return; // the alarm is already queued
957 }
958 mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, id));
959 }
960
961 /******************************************************************************
962 * Start processing the execution queue.
963 */
startProcessQueue(bool evenIfStarted)964 void KAlarmApp::startProcessQueue(bool evenIfStarted)
965 {
966 if (!mInitialised || evenIfStarted)
967 {
968 qCDebug(KALARM_LOG) << "KAlarmApp::startProcessQueue";
969 mInitialised = true;
970 // Process anything already queued.
971 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
972 }
973 }
974
975 /******************************************************************************
976 * The main processing loop for KAlarm.
977 * All KAlarm operations involving opening or updating calendar files are called
978 * from this loop to ensure that only one operation is active at any one time.
979 * This precaution is necessary because KAlarm's activities are mostly
980 * asynchronous, being in response to D-Bus calls from other programs or timer
981 * events, any of which can be received in the middle of performing another
982 * operation. If a calendar file is opened or updated while another calendar
983 * operation is in progress, the program has been observed to hang, or the first
984 * calendar call has failed with data loss - clearly unacceptable!!
985 */
processQueue()986 void KAlarmApp::processQueue()
987 {
988 if (mInitialised && !mProcessingQueue)
989 {
990 qCDebug(KALARM_LOG) << "KAlarmApp::processQueue";
991 mProcessingQueue = true;
992
993 // Refresh alarms if that's been queued
994 KAlarm::refreshAlarmsIfQueued();
995
996 // Process queued events
997 while (!mActionQueue.isEmpty())
998 {
999 ActionQEntry& entry = mActionQueue.head();
1000
1001 // If the first action's resource ID is a string, can't process it
1002 // until its numeric resource ID can be found.
1003 if (!entry.resourceId.isEmpty())
1004 {
1005 if (!Resources::allCreated())
1006 {
1007 // If resource population has timed out, discard all queued events.
1008 if (mResourcesTimedOut)
1009 {
1010 qCCritical(KALARM_LOG) << "Error! Timeout creating calendars";
1011 mActionQueue.clear();
1012 }
1013 break;
1014 }
1015 // Convert the resource ID string to the numeric resource ID.
1016 entry.eventId.setResourceId(EventId::getResourceId(entry.resourceId));
1017 entry.resourceId.clear();
1018 }
1019
1020 // Can't process the first action until its resource has been populated.
1021 const ResourceId id = entry.eventId.resourceId();
1022 if ((id < 0 && !Resources::allPopulated())
1023 || (id >= 0 && !Resources::resource(id).isPopulated()))
1024 {
1025 // If resource population has timed out, discard all queued events.
1026 if (mResourcesTimedOut)
1027 {
1028 qCCritical(KALARM_LOG) << "Error! Timeout reading calendars";
1029 mActionQueue.clear();
1030 }
1031 break;
1032 }
1033
1034 // Process the first action in the queue.
1035 const bool findUniqueId = int(entry.action) & int(QueuedAction::FindId);
1036 bool exitAfter = int(entry.action) & int(QueuedAction::Exit);
1037 const bool exitAfterError = int(entry.action) & int(QueuedAction::ErrorExit);
1038 const bool commandLine = int(entry.action) & int(QueuedAction::CmdLine);
1039 const auto action = static_cast<QueuedAction>(int(entry.action) & int(QueuedAction::ActionMask));
1040
1041 bool ok = true;
1042 bool inhibit = false;
1043 if (entry.eventId.isEmpty())
1044 {
1045 // It's a new alarm
1046 switch (action)
1047 {
1048 case QueuedAction::Trigger:
1049 if (execAlarm(entry.event, entry.event.firstAlarm()) == (void*)-2)
1050 inhibit = true;
1051 break;
1052 case QueuedAction::Handle:
1053 {
1054 Resource resource = Resources::destination(CalEvent::ACTIVE, nullptr, Resources::NoResourcePrompt | Resources::UseOnlyResource);
1055 if (!resource.isValid())
1056 {
1057 qCWarning(KALARM_LOG) << "KAlarmApp::processQueue: Error! Cannot create alarm (no default calendar is defined)";
1058
1059 if (commandLine)
1060 {
1061 const QString errmsg = xi18nc("@info:shell", "Cannot create alarm: No default calendar is defined");
1062 std::cerr << qPrintable(errmsg) << std::endl;
1063 }
1064 ok = false;
1065 }
1066 else
1067 {
1068 const KAlarm::UpdateResult result = KAlarm::addEvent(entry.event, resource, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT);
1069 if (result >= KAlarm::UPDATE_ERROR)
1070 {
1071 //TODO: display error message for command line action, but first ensure that one is returned by addEvent()!
1072 #if 0
1073 if (commandLine)
1074 {
1075 const QString errmsg = xi18nc("@info:shell", "Error creating alarm");
1076 std::cerr << errmsg.toLocal8Bit().data();
1077 }
1078 #endif
1079 ok = false;
1080 }
1081 }
1082 if (!ok && exitAfterError)
1083 exitAfter = true;
1084 break;
1085 }
1086 case QueuedAction::List:
1087 {
1088 const QStringList alarms = scheduledAlarmList();
1089 for (const QString& alarm : alarms)
1090 std::cout << qUtf8Printable(alarm) << std::endl;
1091 break;
1092 }
1093 default:
1094 break;
1095 }
1096 }
1097 else
1098 {
1099 if (action == QueuedAction::Edit)
1100 {
1101 int editingCmdLineAlarm = mEditingCmdLineAlarm & 3;
1102 bool keepQueued = editingCmdLineAlarm <= 1;
1103 switch (editingCmdLineAlarm)
1104 {
1105 case 0:
1106 // Initiate editing an alarm specified on the command line.
1107 mEditingCmdLineAlarm |= 1;
1108 QTimer::singleShot(0, this, &KAlarmApp::slotEditAlarmById); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1109 break;
1110 case 1:
1111 // Currently editing the alarm.
1112 break;
1113 case 2:
1114 // The edit has completed.
1115 mEditingCmdLineAlarm = 0;
1116 break;
1117 default:
1118 break;
1119 }
1120 if (keepQueued)
1121 break;
1122 }
1123 else
1124 {
1125 // Trigger the event if it's due.
1126 const int result = handleEvent(entry.eventId, action, findUniqueId);
1127 if (!result)
1128 inhibit = true;
1129 else if (result < 0 && exitAfter)
1130 {
1131 CommandOptions::printError(xi18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not unique", mCommandOption, entry.eventId.eventId()));
1132 ok = false;
1133 }
1134 }
1135 }
1136
1137 mActionQueue.dequeue();
1138
1139 if (inhibit)
1140 {
1141 // It's a display event which can't be executed because notifications
1142 // are inhibited. Move it to the inhibited queue until the inhibition
1143 // is removed.
1144 }
1145 else if (exitAfter)
1146 {
1147 mProcessingQueue = false; // don't inhibit processing if there is another instance
1148 quitIf((ok ? 0 : 1), exitAfterError);
1149 return; // quitIf() can sometimes return, despite calling exit()
1150 }
1151 }
1152
1153 // Purge the default archived alarms resource if it's time to do so
1154 if (mPurgeDaysQueued >= 0)
1155 {
1156 KAlarm::purgeArchive(mPurgeDaysQueued);
1157 mPurgeDaysQueued = -1;
1158 }
1159
1160 // Now that the queue has been processed, quit if a quit was queued
1161 if (mPendingQuit)
1162 {
1163 if (quitIf(mPendingQuitCode))
1164 return; // quitIf() can sometimes return, despite calling exit()
1165 }
1166
1167 mProcessingQueue = false;
1168
1169 if (!mEditingCmdLineAlarm)
1170 {
1171 // Schedule the application to be woken when the next alarm is due
1172 checkNextDueAlarm();
1173 }
1174 }
1175 }
1176
1177 /******************************************************************************
1178 * Called when a repeat-at-login alarm has been added externally.
1179 * Queues the alarm for triggering.
1180 * First, cancel any scheduled reminder or deferral for it, since these will be
1181 * superseded by the new at-login trigger.
1182 */
atLoginEventAdded(const KAEvent & event)1183 void KAlarmApp::atLoginEventAdded(const KAEvent& event)
1184 {
1185 KAEvent ev = event;
1186 if (!cancelReminderAndDeferral(ev))
1187 {
1188 if (mAlarmsEnabled)
1189 {
1190 mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, EventId(ev)));
1191 if (mInitialised)
1192 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1193 }
1194 }
1195 }
1196
1197 /******************************************************************************
1198 * Called when the system tray main window is closed.
1199 */
removeWindow(TrayWindow *)1200 void KAlarmApp::removeWindow(TrayWindow*)
1201 {
1202 mTrayWindow = nullptr;
1203 }
1204
1205 /******************************************************************************
1206 * Display or close the system tray icon.
1207 */
displayTrayIcon(bool show,MainWindow * parent)1208 bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent)
1209 {
1210 qCDebug(KALARM_LOG) << "KAlarmApp::displayTrayIcon";
1211 static bool creating = false;
1212 if (show)
1213 {
1214 if (!mTrayWindow && !creating)
1215 {
1216 if (!QSystemTrayIcon::isSystemTrayAvailable())
1217 return false;
1218 if (!MainWindow::count())
1219 {
1220 // We have to have at least one main window to act
1221 // as parent to the system tray icon (even if the
1222 // window is hidden).
1223 creating = true; // prevent main window constructor from creating an additional tray icon
1224 parent = MainWindow::create();
1225 creating = false;
1226 }
1227 mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow());
1228 connect(mTrayWindow, &TrayWindow::deleted, this, &KAlarmApp::trayIconToggled);
1229 Q_EMIT trayIconToggled();
1230
1231 if (!checkSystemTray())
1232 quitIf(0); // exit the application if there are no open windows
1233 }
1234 }
1235 else
1236 {
1237 delete mTrayWindow;
1238 mTrayWindow = nullptr;
1239 }
1240 return true;
1241 }
1242
1243 /******************************************************************************
1244 * Check whether the system tray icon has been housed in the system tray.
1245 */
checkSystemTray()1246 bool KAlarmApp::checkSystemTray()
1247 {
1248 if (!mTrayWindow)
1249 return true;
1250 if (QSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray)
1251 {
1252 qCDebug(KALARM_LOG) << "KAlarmApp::checkSystemTray: changed ->" << mNoSystemTray;
1253 mNoSystemTray = !mNoSystemTray;
1254
1255 // Store the new setting in the config file, so that if KAlarm exits it will
1256 // restart with the correct default.
1257 KConfigGroup config(KSharedConfig::openConfig(), "General");
1258 config.writeEntry("NoSystemTray", mNoSystemTray);
1259 config.sync();
1260
1261 // Update other settings
1262 slotShowInSystemTrayChanged();
1263 }
1264 return !mNoSystemTray;
1265 }
1266
1267 /******************************************************************************
1268 * Return the main window associated with the system tray icon.
1269 */
trayMainWindow() const1270 MainWindow* KAlarmApp::trayMainWindow() const
1271 {
1272 return mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr;
1273 }
1274
1275 /******************************************************************************
1276 * Called when the show-in-system-tray preference setting has changed, to show
1277 * or hide the system tray icon.
1278 */
slotShowInSystemTrayChanged()1279 void KAlarmApp::slotShowInSystemTrayChanged()
1280 {
1281 const bool newShowInSysTray = wantShowInSystemTray();
1282 if (newShowInSysTray != mOldShowInSystemTray)
1283 {
1284 // The system tray run mode has changed
1285 ++mActiveCount; // prevent the application from quitting
1286 MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr;
1287 delete mTrayWindow; // remove the system tray icon if it is currently shown
1288 mTrayWindow = nullptr;
1289 mOldShowInSystemTray = newShowInSysTray;
1290 if (newShowInSysTray)
1291 {
1292 // Show the system tray icon
1293 displayTrayIcon(true);
1294 }
1295 else
1296 {
1297 // Stop showing the system tray icon
1298 if (win && win->isHidden())
1299 {
1300 if (MainWindow::count() > 1)
1301 delete win;
1302 else
1303 {
1304 win->setWindowState(win->windowState() | Qt::WindowMinimized);
1305 win->show();
1306 }
1307 }
1308 }
1309 --mActiveCount;
1310 }
1311 }
1312
1313 /******************************************************************************
1314 * Called when the start-of-day time preference setting has changed.
1315 * Change alarm times for date-only alarms.
1316 */
changeStartOfDay()1317 void KAlarmApp::changeStartOfDay()
1318 {
1319 DateTime::setStartOfDay(Preferences::startOfDay());
1320 KAEvent::setStartOfDay(Preferences::startOfDay());
1321 Resources::adjustStartOfDay();
1322 DisplayCalendar::adjustStartOfDay();
1323 }
1324
1325 /******************************************************************************
1326 * Called when the default alarm message font preference setting has changed.
1327 * Notify KAEvent.
1328 */
slotMessageFontChanged(const QFont & font)1329 void KAlarmApp::slotMessageFontChanged(const QFont& font)
1330 {
1331 KAEvent::setDefaultFont(font);
1332 }
1333
1334 /******************************************************************************
1335 * Called when the working time preference settings have changed.
1336 * Notify KAEvent.
1337 */
slotWorkTimeChanged(const QTime & start,const QTime & end,const QBitArray & days)1338 void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days)
1339 {
1340 KAEvent::setWorkTime(days, start, end);
1341 }
1342
1343 /******************************************************************************
1344 * Called when the holiday region preference setting has changed.
1345 * Notify KAEvent.
1346 */
slotHolidaysChanged(const KHolidays::HolidayRegion & holidays)1347 void KAlarmApp::slotHolidaysChanged(const KHolidays::HolidayRegion& holidays)
1348 {
1349 KAEvent::setHolidays(holidays);
1350 }
1351
1352 /******************************************************************************
1353 * Called when the date for February 29th recurrences has changed in the
1354 * preferences settings.
1355 */
slotFeb29TypeChanged(Preferences::Feb29Type type)1356 void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type)
1357 {
1358 KARecurrence::Feb29Type rtype;
1359 switch (type)
1360 {
1361 default:
1362 case Preferences::Feb29_None: rtype = KARecurrence::Feb29_None; break;
1363 case Preferences::Feb29_Feb28: rtype = KARecurrence::Feb29_Feb28; break;
1364 case Preferences::Feb29_Mar1: rtype = KARecurrence::Feb29_Mar1; break;
1365 }
1366 KARecurrence::setDefaultFeb29Type(rtype);
1367 }
1368
1369 /******************************************************************************
1370 * Return whether the program is configured to be running in the system tray.
1371 */
wantShowInSystemTray() const1372 bool KAlarmApp::wantShowInSystemTray() const
1373 {
1374 return Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable();
1375 }
1376
1377 /******************************************************************************
1378 * Set a timeout for populating resources.
1379 */
setResourcesTimeout()1380 void KAlarmApp::setResourcesTimeout()
1381 {
1382 QTimer::singleShot(RESOURCES_TIMEOUT * 1000, this, &KAlarmApp::slotResourcesTimeout); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1383 }
1384
1385 /******************************************************************************
1386 * Called on a timeout to check whether resources have been populated.
1387 * If not, exit the program with code 1.
1388 */
slotResourcesTimeout()1389 void KAlarmApp::slotResourcesTimeout()
1390 {
1391 if (!Resources::allPopulated())
1392 {
1393 // Resource population has timed out.
1394 mResourcesTimedOut = true;
1395 quitIf(1);
1396 }
1397 }
1398
1399 /******************************************************************************
1400 * Called when all resources have been created at startup.
1401 * Check whether there are any writable active calendars, and if not, warn the
1402 * user.
1403 * If alarms are being archived, check whether there is a default archived
1404 * calendar, and if not, warn the user.
1405 */
slotResourcesCreated()1406 void KAlarmApp::slotResourcesCreated()
1407 {
1408 showRestoredWindows(); // display message windows restored at startup.
1409 checkWritableCalendar();
1410 checkArchivedCalendar();
1411 processQueue();
1412 }
1413
1414 /******************************************************************************
1415 * Called when all calendars have been fetched at startup, or calendar migration
1416 * has completed.
1417 * Check whether there are any writable active calendars, and if not, warn the
1418 * user.
1419 */
checkWritableCalendar()1420 void KAlarmApp::checkWritableCalendar()
1421 {
1422 if (mReadOnly)
1423 return; // don't need write access to calendars
1424 if (!Resources::allCreated()
1425 || !DataModel::isMigrationComplete())
1426 return;
1427 static bool done = false;
1428 if (done)
1429 return;
1430 done = true;
1431 qCDebug(KALARM_LOG) << "KAlarmApp::checkWritableCalendar";
1432
1433 // Check for, and remove, any duplicate resources, i.e. those which use the
1434 // same calendar file/directory.
1435 DataModel::removeDuplicateResources();
1436
1437 // Find whether there are any writable active alarm calendars
1438 const bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty();
1439 if (!active)
1440 {
1441 qCWarning(KALARM_LOG) << "KAlarmApp::checkWritableCalendar: No writable active calendar";
1442 KAMessageBox::information(MainWindow::mainMainWindow(),
1443 xi18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled.<nl/><nl/>"
1444 "To fix this, use <interface>View | Show Calendars</interface> to check or change calendar statuses."),
1445 QString(), QStringLiteral("noWritableCal"));
1446 }
1447 }
1448
1449 /******************************************************************************
1450 * If alarms are being archived, check whether there is a default archived
1451 * calendar, and if not, warn the user.
1452 */
checkArchivedCalendar()1453 void KAlarmApp::checkArchivedCalendar()
1454 {
1455 static bool done = false;
1456 if (done)
1457 return;
1458 done = true;
1459
1460 // If alarms are to be archived, check that the default archived alarm
1461 // calendar is writable.
1462 if (Preferences::archivedKeepDays())
1463 {
1464 Resource standard = Resources::getStandard(CalEvent::ARCHIVED, true);
1465 if (!standard.isValid())
1466 {
1467 // Schedule the display of a user prompt, without holding up
1468 // other processing.
1469 QTimer::singleShot(0, this, &KAlarmApp::promptArchivedCalendar); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1470 }
1471 }
1472 }
1473
1474 /******************************************************************************
1475 * Edit an alarm specified on the command line.
1476 */
slotEditAlarmById()1477 void KAlarmApp::slotEditAlarmById()
1478 {
1479 qCDebug(KALARM_LOG) << "KAlarmApp::slotEditAlarmById";
1480 ActionQEntry& entry = mActionQueue.head();
1481 if (!KAlarm::editAlarmById(entry.eventId))
1482 {
1483 CommandOptions::printError(xi18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not editable", mCommandOption, entry.eventId.eventId()));
1484 mActionQueue.clear();
1485 quitIf(1);
1486 }
1487 else
1488 {
1489 createOnlyMainWindow(); // prevent the application from quitting
1490 if (mEditingCmdLineAlarm & 0x10)
1491 {
1492 mRedisplayAlarms = true;
1493 showRestoredWindows();
1494 }
1495 mEditingCmdLineAlarm = 2; // indicate edit completion
1496 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1497 }
1498 }
1499
1500 /******************************************************************************
1501 * If alarms are being archived, check whether there is a default archived
1502 * calendar, and if not, warn the user.
1503 */
promptArchivedCalendar()1504 void KAlarmApp::promptArchivedCalendar()
1505 {
1506 const bool archived = !Resources::enabledResources(CalEvent::ARCHIVED, true).isEmpty();
1507 if (archived)
1508 {
1509 qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no writable archived calendar";
1510 KAMessageBox::information(MainWindow::mainMainWindow(),
1511 xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no writable archived alarm calendar is enabled.<nl/><nl/>"
1512 "To fix this, use <interface>View | Show Calendars</interface> to check or change calendar statuses."),
1513 QString(), QStringLiteral("noWritableArch"));
1514 }
1515 else
1516 {
1517 qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no standard archived calendar";
1518 KAMessageBox::information(MainWindow::mainMainWindow(),
1519 xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no archived alarm calendar is set as default.<nl/><nl/>"
1520 "To fix this, use <interface>View | Show Calendars</interface>, select an archived alarms calendar, and check <interface>Use as Default for Archived Alarms</interface>."),
1521 QString(), QStringLiteral("noStandardArch"));
1522 }
1523 }
1524
1525 /******************************************************************************
1526 * Called when a new resource has been added, to note the possible need to purge
1527 * its old alarms if it is the default archived calendar.
1528 */
slotResourceAdded(const Resource & resource)1529 void KAlarmApp::slotResourceAdded(const Resource& resource)
1530 {
1531 if (resource.alarmTypes() & CalEvent::ARCHIVED)
1532 mPendingPurges += resource.id();
1533 }
1534
1535 /******************************************************************************
1536 * Called when a resource has been populated, to purge its old alarms if it is
1537 * the default archived calendar.
1538 */
slotResourcePopulated(const Resource & resource)1539 void KAlarmApp::slotResourcePopulated(const Resource& resource)
1540 {
1541 if (mPendingPurges.removeAll(resource.id()) > 0)
1542 purgeNewArchivedDefault(resource);
1543 }
1544
1545 /******************************************************************************
1546 * Called when a new resource has been populated, or when a resource has been
1547 * set as the standard resource for its type.
1548 * If it is the default archived calendar, purge its old alarms if necessary.
1549 */
purgeNewArchivedDefault(const Resource & resource)1550 void KAlarmApp::purgeNewArchivedDefault(const Resource& resource)
1551 {
1552 if (Resources::isStandard(resource, CalEvent::ARCHIVED))
1553 {
1554 qCDebug(KALARM_LOG) << "KAlarmApp::purgeNewArchivedDefault:" << resource.displayId() << ": standard archived...";
1555 if (mArchivedPurgeDays >= 0)
1556 purge(mArchivedPurgeDays);
1557 else
1558 setArchivePurgeDays();
1559 }
1560 }
1561
1562 /******************************************************************************
1563 * Called when the length of time to keep archived alarms changes in KAlarm's
1564 * preferences.
1565 * Set the number of days to keep archived alarms.
1566 * Alarms which are older are purged immediately, and at the start of each day.
1567 */
setArchivePurgeDays()1568 void KAlarmApp::setArchivePurgeDays()
1569 {
1570 const int newDays = Preferences::archivedKeepDays();
1571 if (newDays != mArchivedPurgeDays)
1572 {
1573 const int oldDays = mArchivedPurgeDays;
1574 mArchivedPurgeDays = newDays;
1575 if (mArchivedPurgeDays <= 0)
1576 StartOfDayTimer::disconnect(this);
1577 if (mArchivedPurgeDays < 0)
1578 return; // keep indefinitely, so don't purge
1579 if (oldDays < 0 || mArchivedPurgeDays < oldDays)
1580 {
1581 // Alarms are now being kept for less long, so purge them
1582 purge(mArchivedPurgeDays);
1583 if (!mArchivedPurgeDays)
1584 return; // don't archive any alarms
1585 }
1586 // Start the purge timer to expire at the start of the next day
1587 // (using the user-defined start-of-day time).
1588 StartOfDayTimer::connect(this, SLOT(slotPurge()));
1589 }
1590 }
1591
1592 /******************************************************************************
1593 * Purge all archived events from the calendar whose end time is longer ago than
1594 * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero.
1595 */
purge(int daysToKeep)1596 void KAlarmApp::purge(int daysToKeep)
1597 {
1598 if (mPurgeDaysQueued < 0 || daysToKeep < mPurgeDaysQueued)
1599 mPurgeDaysQueued = daysToKeep;
1600
1601 // Do the purge once any other current operations are completed
1602 processQueue();
1603 }
1604
1605 /******************************************************************************
1606 * Called when the Freedesktop notifications properties have changed.
1607 * Check whether the inhibited property has changed.
1608 */
slotFDOPropertiesChanged(const QString & interface,const QVariantMap & changedProperties,const QStringList & invalidatedProperties)1609 void KAlarmApp::slotFDOPropertiesChanged(const QString& interface,
1610 const QVariantMap& changedProperties,
1611 const QStringList& invalidatedProperties)
1612 {
1613 Q_UNUSED(interface); // always "org.freedesktop.Notifications"
1614 Q_UNUSED(invalidatedProperties);
1615 const auto it = changedProperties.find(QStringLiteral("Inhibited"));
1616 if (it != changedProperties.end())
1617 {
1618 const bool inhibited = it.value().toBool();
1619 if (inhibited != mNotificationsInhibited)
1620 {
1621 qCDebug(KALARM_LOG) << "KAlarmApp::slotFDOPropertiesChanged: Notifications inhibited ->" << inhibited;
1622 mNotificationsInhibited = inhibited;
1623 if (!mNotificationsInhibited)
1624 {
1625 showRestoredWindows(); // display message windows restored at startup.
1626 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1627 }
1628 }
1629 }
1630 }
1631
1632 /******************************************************************************
1633 * Output a list of pending alarms, with their next scheduled occurrence.
1634 */
scheduledAlarmList()1635 QStringList KAlarmApp::scheduledAlarmList()
1636 {
1637 QStringList alarms;
1638 const QVector<KAEvent> events = KAlarm::getSortedActiveEvents(this);
1639 for (const KAEvent& event : events)
1640 {
1641 const KADateTime dateTime = event.nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone();
1642 const Resource resource = Resources::resource(event.resourceId());
1643 QString text(resource.configName() + QLatin1String(":"));
1644 text += event.id() + QLatin1Char(' ')
1645 + dateTime.toString(QStringLiteral("%Y%m%dT%H%M "))
1646 + AlarmText::summary(event, 1);
1647 alarms << text;
1648 }
1649 return alarms;
1650 }
1651
1652 /******************************************************************************
1653 * Enable or disable alarm monitoring.
1654 */
setAlarmsEnabled(bool enabled)1655 void KAlarmApp::setAlarmsEnabled(bool enabled)
1656 {
1657 if (enabled != mAlarmsEnabled)
1658 {
1659 mAlarmsEnabled = enabled;
1660 Q_EMIT alarmEnabledToggled(enabled);
1661 if (!enabled)
1662 KAlarm::cancelRtcWake(nullptr);
1663 else if (!mProcessingQueue)
1664 checkNextDueAlarm();
1665 }
1666 }
1667
1668 /******************************************************************************
1669 * Spread or collect alarm message and error message windows.
1670 */
spreadWindows(bool spread)1671 void KAlarmApp::spreadWindows(bool spread)
1672 {
1673 spread = MessageWindow::spread(spread);
1674 Q_EMIT spreadWindowsToggled(spread);
1675 }
1676
1677 /******************************************************************************
1678 * Called when the spread status of message windows changes.
1679 * Set the 'spread windows' action state.
1680 */
setSpreadWindowsState(bool spread)1681 void KAlarmApp::setSpreadWindowsState(bool spread)
1682 {
1683 Q_EMIT spreadWindowsToggled(spread);
1684 }
1685
1686 /******************************************************************************
1687 * Check whether the window manager's handling of keyboard focus transfer
1688 * between application windows is broken. This is true for Ubuntu's Unity
1689 * desktop, where MessageWindow windows steal keyboard focus from EditAlarmDlg
1690 * windows.
1691 */
windowFocusBroken() const1692 bool KAlarmApp::windowFocusBroken() const
1693 {
1694 return mWindowFocusBroken;
1695 }
1696
1697 /******************************************************************************
1698 * Check whether window/keyboard focus currently needs to be fixed manually due
1699 * to the window manager not handling it correctly. This will occur if there are
1700 * both EditAlarmDlg and MessageWindow windows currently active.
1701 */
needWindowFocusFix() const1702 bool KAlarmApp::needWindowFocusFix() const
1703 {
1704 return mWindowFocusBroken && MessageWindow::windowCount(true) && EditAlarmDlg::instanceCount();
1705 }
1706
1707 /******************************************************************************
1708 * Called to schedule a new alarm, either in response to a D-Bus notification or
1709 * to command line options.
1710 * Reply = true unless there was a parameter error or an error opening calendar file.
1711 */
scheduleEvent(QueuedAction queuedActionFlags,KAEvent::SubAction action,const QString & name,const QString & text,const KADateTime & dateTime,int lateCancel,KAEvent::Flags flags,const QColor & bg,const QColor & fg,const QFont & font,const QString & audioFile,float audioVolume,int reminderMinutes,const KARecurrence & recurrence,const KCalendarCore::Duration & repeatInterval,int repeatCount,uint mailFromID,const KCalendarCore::Person::List & mailAddresses,const QString & mailSubject,const QStringList & mailAttachments)1712 bool KAlarmApp::scheduleEvent(QueuedAction queuedActionFlags,
1713 KAEvent::SubAction action, const QString& name, const QString& text,
1714 const KADateTime& dateTime, int lateCancel, KAEvent::Flags flags,
1715 const QColor& bg, const QColor& fg, const QFont& font,
1716 const QString& audioFile, float audioVolume, int reminderMinutes,
1717 const KARecurrence& recurrence, const KCalendarCore::Duration& repeatInterval, int repeatCount,
1718 uint mailFromID, const KCalendarCore::Person::List& mailAddresses,
1719 const QString& mailSubject, const QStringList& mailAttachments)
1720 {
1721 if (!dateTime.isValid())
1722 {
1723 qCWarning(KALARM_LOG) << "KAlarmApp::scheduleEvent: Error! Invalid time" << text;
1724 return false;
1725 }
1726 const KADateTime now = KADateTime::currentUtcDateTime();
1727 if (lateCancel && dateTime < now.addSecs(-maxLateness(lateCancel)))
1728 {
1729 qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: not executed (late-cancel)" << text;
1730 return true; // alarm time was already archived too long ago
1731 }
1732 KADateTime alarmTime = dateTime;
1733 // Round down to the nearest minute to avoid scheduling being messed up
1734 if (!dateTime.isDateOnly())
1735 alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0));
1736
1737 KAEvent event(alarmTime, name, text, bg, fg, font, action, lateCancel, flags, true);
1738 if (reminderMinutes)
1739 {
1740 const bool onceOnly = flags & KAEvent::REMINDER_ONCE;
1741 event.setReminder(reminderMinutes, onceOnly);
1742 }
1743 if (!audioFile.isEmpty())
1744 event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1);
1745 if (!mailAddresses.isEmpty())
1746 event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments);
1747 event.setRecurrence(recurrence);
1748 event.setFirstRecurrence();
1749 event.setRepetition(Repetition(repeatInterval, repeatCount - 1));
1750 event.endChanges();
1751 if (alarmTime <= now)
1752 {
1753 // Alarm is due for execution already.
1754 // First execute it once without adding it to the calendar file.
1755 qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: executing" << text;
1756 if (!mInitialised
1757 || execAlarm(event, event.firstAlarm()) == (void*)-2)
1758 mActionQueue.enqueue(ActionQEntry(event, QueuedAction::Trigger));
1759 // If it's a recurring alarm, reschedule it for its next occurrence
1760 if (!event.recurs()
1761 || event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE)
1762 return true;
1763 // It has recurrences in the future
1764 }
1765
1766 // Queue the alarm for insertion into the calendar file
1767 qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent: creating new alarm" << text;
1768 const QueuedAction qaction = static_cast<QueuedAction>(int(QueuedAction::Handle) + int(queuedActionFlags));
1769 mActionQueue.enqueue(ActionQEntry(event, qaction));
1770 if (mInitialised)
1771 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1772 return true;
1773 }
1774
1775 /******************************************************************************
1776 * Called in response to a D-Bus request to trigger or cancel an event.
1777 * Optionally display the event. Delete the event from the calendar file and
1778 * from every main window instance.
1779 */
dbusHandleEvent(const EventId & eventID,QueuedAction action)1780 bool KAlarmApp::dbusHandleEvent(const EventId& eventID, QueuedAction action)
1781 {
1782 qCDebug(KALARM_LOG) << "KAlarmApp::dbusHandleEvent:" << eventID;
1783 mActionQueue.append(ActionQEntry(action, eventID));
1784 if (mInitialised)
1785 QTimer::singleShot(0, this, &KAlarmApp::processQueue); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1786 return true;
1787 }
1788
1789 /******************************************************************************
1790 * Called in response to a D-Bus request to list all pending alarms.
1791 */
dbusList()1792 QString KAlarmApp::dbusList()
1793 {
1794 qCDebug(KALARM_LOG) << "KAlarmApp::dbusList";
1795 return scheduledAlarmList().join(QLatin1Char('\n')) + QLatin1Char('\n');
1796 }
1797
1798 /******************************************************************************
1799 * Either:
1800 * a) Execute the event if it's due, and then delete it if it has no outstanding
1801 * repetitions.
1802 * b) Delete the event.
1803 * c) Reschedule the event for its next repetition. If none remain, delete it.
1804 * If the event is deleted, it is removed from the calendar file and from every
1805 * main window instance.
1806 * If 'findUniqueId' is true and 'id' does not specify a resource, all resources
1807 * will be searched for the event's unique ID.
1808 * Reply = -1 if event ID not found, or if more than one event with the same ID
1809 * is found.
1810 * = 0 if can't trigger display event because notifications are inhibited.
1811 * = 1 if success.
1812 */
handleEvent(const EventId & id,QueuedAction action,bool findUniqueId)1813 int KAlarmApp::handleEvent(const EventId& id, QueuedAction action, bool findUniqueId)
1814 {
1815 Q_ASSERT(!(int(action) & ~int(QueuedAction::ActionMask)));
1816
1817 // Delete any expired wake-on-suspend config data
1818 KAlarm::checkRtcWakeConfig();
1819
1820 const QString eventID(id.eventId());
1821 KAEvent event = ResourcesCalendar::event(id, findUniqueId);
1822 if (!event.isValid())
1823 {
1824 if (id.resourceId() != -1)
1825 qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found:" << eventID;
1826 else if (findUniqueId)
1827 qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found, or duplicated:" << eventID;
1828 else
1829 qCCritical(KALARM_LOG) << "KAlarmApp::handleEvent: No resource ID specified for event:" << eventID;
1830 return -1;
1831 }
1832 switch (action)
1833 {
1834 case QueuedAction::Cancel:
1835 {
1836 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << ", CANCEL";
1837 Resource resource = Resources::resource(event.resourceId());
1838 KAlarm::deleteEvent(event, resource, true);
1839 break;
1840 }
1841 case QueuedAction::Trigger: // handle it if it's due, else execute it regardless
1842 case QueuedAction::Handle: // handle it if it's due
1843 {
1844 const KADateTime now = KADateTime::currentUtcDateTime();
1845 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << "," << (action==QueuedAction::Trigger?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC";
1846 bool updateCalAndDisplay = false;
1847 bool alarmToExecuteValid = false;
1848 KAAlarm alarmToExecute;
1849 bool restart = false;
1850 // Check all the alarms in turn.
1851 // Note that the main alarm is fetched before any other alarms.
1852 for (KAAlarm alarm = event.firstAlarm();
1853 alarm.isValid();
1854 alarm = (restart ? event.firstAlarm() : event.nextAlarm(alarm)), restart = false)
1855 {
1856 // Check if the alarm is due yet.
1857 const KADateTime nextDT = alarm.dateTime(true).effectiveKDateTime();
1858 const int secs = nextDT.secsTo(now);
1859 if (secs < 0)
1860 {
1861 // The alarm appears to be in the future.
1862 // Check if it's an invalid local time during a daylight
1863 // saving time shift, which has actually passed.
1864 if (alarm.dateTime().timeSpec() != KADateTime::LocalZone
1865 || nextDT > now.toTimeSpec(KADateTime::LocalZone))
1866 {
1867 // This alarm is definitely not due yet
1868 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not due";
1869 continue;
1870 }
1871 }
1872 bool reschedule = false;
1873 bool rescheduleWork = false;
1874 if ((event.workTimeOnly() || event.holidaysExcluded()) && !alarm.deferred())
1875 {
1876 // The alarm is restricted to working hours and/or non-holidays
1877 // (apart from deferrals). This needs to be re-evaluated every
1878 // time it triggers, since working hours could change.
1879 if (alarm.dateTime().isDateOnly())
1880 {
1881 KADateTime dt(nextDT);
1882 dt.setDateOnly(true);
1883 reschedule = event.excludedByWorkTimeOrHoliday(dt);
1884 }
1885 else
1886 reschedule = event.excludedByWorkTimeOrHoliday(nextDT);
1887 rescheduleWork = reschedule;
1888 if (reschedule)
1889 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not during working hours";
1890 }
1891 if (!reschedule && alarm.repeatAtLogin())
1892 {
1893 // Alarm is to be displayed at every login.
1894 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: REPEAT_AT_LOGIN";
1895 // Check if the main alarm is already being displayed.
1896 // (We don't want to display both at the same time.)
1897 if (alarmToExecute.isValid())
1898 continue;
1899
1900 // Set the time to display if it's a display alarm
1901 alarm.setTime(now);
1902 }
1903 if (!reschedule && event.lateCancel())
1904 {
1905 // Alarm is due, and it is to be cancelled if too late.
1906 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: LATE_CANCEL";
1907 bool cancel = false;
1908 if (alarm.dateTime().isDateOnly())
1909 {
1910 // The alarm has no time, so cancel it if its date is too far past
1911 const int maxlate = event.lateCancel() / 1440; // maximum lateness in days
1912 KADateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime());
1913 if (now >= limit)
1914 {
1915 // It's too late to display the scheduled occurrence.
1916 // Find the last previous occurrence of the alarm.
1917 DateTime next;
1918 const KAEvent::OccurType type = event.previousOccurrence(now, next, true);
1919 switch (type & ~KAEvent::OCCURRENCE_REPEAT)
1920 {
1921 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
1922 case KAEvent::RECURRENCE_DATE:
1923 case KAEvent::RECURRENCE_DATE_TIME:
1924 case KAEvent::LAST_RECURRENCE:
1925 limit.setDate(next.date().addDays(maxlate + 1));
1926 if (now >= limit)
1927 {
1928 if (type == KAEvent::LAST_RECURRENCE
1929 || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event.recurs()))
1930 cancel = true; // last occurrence (and there are no repetitions)
1931 else
1932 reschedule = true;
1933 }
1934 break;
1935 case KAEvent::NO_OCCURRENCE:
1936 default:
1937 reschedule = true;
1938 break;
1939 }
1940 }
1941 }
1942 else
1943 {
1944 // The alarm is timed. Allow it to be the permitted amount late before cancelling it.
1945 const int maxlate = maxLateness(event.lateCancel());
1946 if (secs > maxlate)
1947 {
1948 // It's over the maximum interval late.
1949 // Find the most recent occurrence of the alarm.
1950 DateTime next;
1951 const KAEvent::OccurType type = event.previousOccurrence(now, next, true);
1952 switch (type & ~KAEvent::OCCURRENCE_REPEAT)
1953 {
1954 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
1955 case KAEvent::RECURRENCE_DATE:
1956 case KAEvent::RECURRENCE_DATE_TIME:
1957 case KAEvent::LAST_RECURRENCE:
1958 if (next.effectiveKDateTime().secsTo(now) > maxlate)
1959 {
1960 if (type == KAEvent::LAST_RECURRENCE
1961 || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event.recurs()))
1962 cancel = true; // last occurrence (and there are no repetitions)
1963 else
1964 reschedule = true;
1965 }
1966 break;
1967 case KAEvent::NO_OCCURRENCE:
1968 default:
1969 reschedule = true;
1970 break;
1971 }
1972 }
1973 }
1974
1975 if (cancel)
1976 {
1977 // All recurrences are finished, so cancel the event
1978 event.setArchive();
1979 if (cancelAlarm(event, alarm.type(), false))
1980 return 1; // event has been deleted
1981 updateCalAndDisplay = true;
1982 continue;
1983 }
1984 }
1985 if (reschedule)
1986 {
1987 // The latest repetition was too long ago, so schedule the next one
1988 switch (rescheduleAlarm(event, alarm, false, (rescheduleWork ? nextDT : KADateTime())))
1989 {
1990 case 1:
1991 // A working-time-only alarm has been rescheduled and the
1992 // rescheduled time is already due. Start processing the
1993 // event again.
1994 alarmToExecuteValid = false;
1995 restart = true;
1996 break;
1997 case -1:
1998 return 1; // event has been deleted
1999 default:
2000 break;
2001 }
2002 updateCalAndDisplay = true;
2003 continue;
2004 }
2005 if (!alarmToExecuteValid)
2006 {
2007 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": execute";
2008 alarmToExecute = alarm; // note the alarm to be displayed
2009 alarmToExecuteValid = true; // only trigger one alarm for the event
2010 }
2011 else
2012 qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": skip";
2013 }
2014
2015 // If there is an alarm to execute, do this last after rescheduling/cancelling
2016 // any others. This ensures that the updated event is only saved once to the calendar.
2017 if (alarmToExecute.isValid())
2018 {
2019 if (execAlarm(event, alarmToExecute, Reschedule | (alarmToExecute.repeatAtLogin() ? NoExecFlag : AllowDefer)) == (void*)-2)
2020 return 0; // display alarm, but notifications are inhibited
2021 }
2022 else
2023 {
2024 if (action == QueuedAction::Trigger)
2025 {
2026 // The alarm is to be executed regardless of whether it's due.
2027 // Only trigger one alarm from the event - we don't want multiple
2028 // identical messages, for example.
2029 const KAAlarm alarm = event.firstAlarm();
2030 if (alarm.isValid())
2031 {
2032 if (execAlarm(event, alarm) == (void*)-2)
2033 return 0; // display alarm, but notifications are inhibited
2034 }
2035 }
2036 if (updateCalAndDisplay)
2037 KAlarm::updateEvent(event); // update the window lists and calendar file
2038 else if (action != QueuedAction::Trigger) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: No action"; }
2039 }
2040 break;
2041 }
2042 default:
2043 break;
2044 }
2045 return 1;
2046 }
2047
2048 /******************************************************************************
2049 * Called when an alarm action has completed, to perform any post-alarm actions.
2050 */
alarmCompleted(const KAEvent & event)2051 void KAlarmApp::alarmCompleted(const KAEvent& event)
2052 {
2053 if (!event.postAction().isEmpty())
2054 {
2055 // doShellCommand() will error if the user is not authorised to run
2056 // shell commands.
2057 const QString command = event.postAction();
2058 qCDebug(KALARM_LOG) << "KAlarmApp::alarmCompleted:" << event.id() << ":" << command;
2059 doShellCommand(command, event, nullptr, ProcData::POST_ACTION);
2060 }
2061 }
2062
2063 /******************************************************************************
2064 * Reschedule the alarm for its next recurrence after now. If none remain,
2065 * delete it. If the alarm is deleted and it is the last alarm for its event,
2066 * the event is removed from the calendar file and from every main window
2067 * instance.
2068 * If 'nextDt' is valid, the event is rescheduled for the next non-working
2069 * time occurrence after that.
2070 * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due
2071 * = -1 if the event has been deleted
2072 * = 0 otherwise.
2073 */
rescheduleAlarm(KAEvent & event,const KAAlarm & alarm,bool updateCalAndDisplay,const KADateTime & nextDt)2074 int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KADateTime& nextDt)
2075 {
2076 qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: Alarm type:" << alarm.type();
2077 int reply = 0;
2078 bool update = false;
2079 event.startChanges();
2080 if (alarm.repeatAtLogin())
2081 {
2082 // Leave an alarm which repeats at every login until its main alarm triggers
2083 if (!event.reminderActive() && event.reminderMinutes() < 0)
2084 {
2085 // Executing an at-login alarm: first schedule the reminder
2086 // which occurs AFTER the main alarm.
2087 event.activateReminderAfter(KADateTime::currentUtcDateTime());
2088 }
2089 // Repeat-at-login alarms are usually unchanged after triggering.
2090 // Ensure that the archive flag (which was set in execAlarm()) is saved.
2091 update = true;
2092 }
2093 else if (alarm.isReminder() || alarm.deferred())
2094 {
2095 // It's a reminder alarm or an extra deferred alarm, so delete it
2096 event.removeExpiredAlarm(alarm.type());
2097 update = true;
2098 }
2099 else
2100 {
2101 // Reschedule the alarm for its next occurrence.
2102 bool cancelled = false;
2103 DateTime last = event.mainDateTime(false); // note this trigger time
2104 if (last != event.mainDateTime(true))
2105 last = DateTime(); // but ignore sub-repetition triggers
2106 bool next = nextDt.isValid();
2107 KADateTime next_dt = nextDt;
2108 const KADateTime now = KADateTime::currentUtcDateTime();
2109 do
2110 {
2111 const KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now);
2112 switch (type)
2113 {
2114 case KAEvent::NO_OCCURRENCE:
2115 // All repetitions are finished, so cancel the event
2116 qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: No occurrence";
2117 if (event.reminderMinutes() < 0 && last.isValid()
2118 && alarm.type() != KAAlarm::AT_LOGIN_ALARM && !event.mainExpired())
2119 {
2120 // Set the reminder which is now due after the last main alarm trigger.
2121 // Note that at-login reminders are scheduled in execAlarm().
2122 event.activateReminderAfter(last);
2123 updateCalAndDisplay = true;
2124 }
2125 if (cancelAlarm(event, alarm.type(), updateCalAndDisplay))
2126 return -1;
2127 break;
2128 default:
2129 if (!(type & KAEvent::OCCURRENCE_REPEAT))
2130 break;
2131 // Next occurrence is a repeat, so fall through to recurrence handling
2132 Q_FALLTHROUGH();
2133 case KAEvent::RECURRENCE_DATE:
2134 case KAEvent::RECURRENCE_DATE_TIME:
2135 case KAEvent::LAST_RECURRENCE:
2136 // The event is due by now and repetitions still remain, so rewrite the event
2137 if (updateCalAndDisplay)
2138 update = true;
2139 break;
2140 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
2141 // The first occurrence is still due?!?, so don't do anything
2142 break;
2143 }
2144 if (cancelled)
2145 break;
2146 if (event.deferred())
2147 {
2148 // Just in case there's also a deferred alarm, ensure it's removed
2149 event.removeExpiredAlarm(KAAlarm::DEFERRED_ALARM);
2150 update = true;
2151 }
2152 if (next)
2153 {
2154 // The alarm is restricted to working hours and/or non-holidays.
2155 // Check if the calculated next time is valid.
2156 next_dt = event.mainDateTime(true).effectiveKDateTime();
2157 if (event.mainDateTime(false).isDateOnly())
2158 {
2159 KADateTime dt(next_dt);
2160 dt.setDateOnly(true);
2161 next = event.excludedByWorkTimeOrHoliday(dt);
2162 }
2163 else
2164 next = event.excludedByWorkTimeOrHoliday(next_dt);
2165 }
2166 } while (next && next_dt <= now);
2167 reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0;
2168
2169 if (event.reminderMinutes() < 0 && last.isValid()
2170 && alarm.type() != KAAlarm::AT_LOGIN_ALARM)
2171 {
2172 // Set the reminder which is now due after the last main alarm trigger.
2173 // Note that at-login reminders are scheduled in execAlarm().
2174 event.activateReminderAfter(last);
2175 }
2176 }
2177 event.endChanges();
2178 if (update)
2179 KAlarm::updateEvent(event, nullptr, true, false); // update the window lists and calendar file
2180 return reply;
2181 }
2182
2183 /******************************************************************************
2184 * Delete the alarm. If it is the last alarm for its event, the event is removed
2185 * from the calendar file and from every main window instance.
2186 * Reply = true if event has been deleted.
2187 */
cancelAlarm(KAEvent & event,KAAlarm::Type alarmType,bool updateCalAndDisplay)2188 bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay)
2189 {
2190 qCDebug(KALARM_LOG) << "KAlarmApp::cancelAlarm";
2191 if (alarmType == KAAlarm::MAIN_ALARM && !event.displaying() && event.toBeArchived())
2192 {
2193 // The event is being deleted. Save it in the archived resource first.
2194 Resource resource;
2195 KAEvent ev(event);
2196 KAlarm::addArchivedEvent(ev, resource);
2197 }
2198 event.removeExpiredAlarm(alarmType);
2199 if (!event.alarmCount())
2200 {
2201 // If it's a command alarm being executed, mark it as deleted
2202 ProcData* pd = findCommandProcess(event.id());
2203 if (pd)
2204 pd->eventDeleted = true;
2205
2206 // Delete it
2207 Resource resource;
2208 KAlarm::deleteEvent(event, resource, false);
2209 return true;
2210 }
2211 if (updateCalAndDisplay)
2212 KAlarm::updateEvent(event); // update the window lists and calendar file
2213 return false;
2214 }
2215
2216 /******************************************************************************
2217 * Cancel any reminder or deferred alarms in an repeat-at-login event.
2218 * This should be called when the event is first loaded.
2219 * If there are no more alarms left in the event, the event is removed from the
2220 * calendar file and from every main window instance.
2221 * Reply = true if event has been deleted.
2222 */
cancelReminderAndDeferral(KAEvent & event)2223 bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event)
2224 {
2225 return cancelAlarm(event, KAAlarm::REMINDER_ALARM, false)
2226 || cancelAlarm(event, KAAlarm::DEFERRED_REMINDER_ALARM, false)
2227 || cancelAlarm(event, KAAlarm::DEFERRED_ALARM, true);
2228 }
2229
2230 /******************************************************************************
2231 * Execute an alarm by displaying its message or file, or executing its command.
2232 * Reply = ShellProcess instance if a command alarm
2233 * = MessageWindow if an audio alarm
2234 * != null if successful
2235 * = -1 if execution has not completed
2236 * = -2 if can't execute display event because notifications are inhibited.
2237 * = null if the alarm is disabled, or if an error message was output.
2238 */
execAlarm(KAEvent & event,const KAAlarm & alarm,ExecAlarmFlags flags)2239 void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, ExecAlarmFlags flags)
2240 {
2241 if (!mAlarmsEnabled || !event.enabled())
2242 {
2243 // The event (or all events) is disabled
2244 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": disabled";
2245 if (flags & Reschedule)
2246 rescheduleAlarm(event, alarm, true);
2247 return nullptr;
2248 }
2249
2250 if (mNotificationsInhibited && !(flags & NoNotifyInhibit)
2251 && (event.actionTypes() & KAEvent::ACT_DISPLAY))
2252 {
2253 // It's a display event and notifications are inhibited.
2254 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": notifications inhibited";
2255 return (void*)-2;
2256 }
2257
2258 void* result = (void*)1;
2259 event.setArchive();
2260
2261 switch (alarm.action())
2262 {
2263 case KAAlarm::COMMAND:
2264 if (!event.commandDisplay())
2265 {
2266 // execCommandAlarm() will error if the user is not authorised
2267 // to run shell commands.
2268 result = execCommandAlarm(event, alarm, flags & NoRecordCmdError);
2269 if (flags & Reschedule)
2270 rescheduleAlarm(event, alarm, true);
2271 break;
2272 }
2273 Q_FALLTHROUGH(); // fall through to MESSAGE
2274 case KAAlarm::MESSAGE:
2275 case KAAlarm::FILE:
2276 {
2277 // Display a message, file or command output, provided that the same event
2278 // isn't already being displayed
2279 MessageDisplay* disp = MessageDisplay::findEvent(EventId(event));
2280 // Find if we're changing a reminder message to the real message
2281 const bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM);
2282 const bool replaceReminder = !reminder && disp && (disp->alarmType() & KAAlarm::REMINDER_ALARM);
2283 if (!reminder
2284 && (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral))
2285 && (replaceReminder || !disp) && !(flags & NoPreAction)
2286 && !event.preAction().isEmpty())
2287 {
2288 // It's not a reminder alarm, and it's not a deferred alarm unless the
2289 // pre-alarm action applies to deferred alarms, and there is no message
2290 // window (other than a reminder window) currently displayed for this
2291 // alarm, and we need to execute a command before displaying the new window.
2292 //
2293 // NOTE: The pre-action is not executed for a recurring alarm if an
2294 // alarm message window for a previous occurrence is still visible.
2295 // Check whether the command is already being executed for this alarm.
2296 for (const ProcData* pd : std::as_const(mCommandProcesses))
2297 {
2298 if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION))
2299 {
2300 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Already executing pre-DISPLAY command";
2301 return pd->process; // already executing - don't duplicate the action
2302 }
2303 }
2304
2305 // doShellCommand() will error if the user is not authorised to run
2306 // shell commands.
2307 const QString command = event.preAction();
2308 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Pre-DISPLAY command:" << command;
2309 const int pdFlags = (flags & Reschedule ? ProcData::RESCHEDULE : 0) | (flags & AllowDefer ? ProcData::ALLOW_DEFER : 0);
2310 if (doShellCommand(command, event, &alarm, (pdFlags | ProcData::PRE_ACTION)))
2311 {
2312 ResourcesCalendar::setAlarmPending(event);
2313 return result; // display the message after the command completes
2314 }
2315 // Error executing command
2316 if (event.extraActionOptions() & KAEvent::CancelOnPreActError)
2317 {
2318 // Cancel the rest of the alarm execution
2319 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": pre-action failed: cancelled";
2320 if (flags & Reschedule)
2321 rescheduleAlarm(event, alarm, true);
2322 return nullptr;
2323 }
2324 // Display the message even though it failed
2325 }
2326
2327 if (!disp)
2328 {
2329 // There isn't already a message for this event
2330 const int mdFlags = (flags & Reschedule ? 0 : MessageDisplay::NoReschedule)
2331 | (flags & AllowDefer ? 0 : MessageDisplay::NoDefer)
2332 | (flags & NoRecordCmdError ? MessageDisplay::NoRecordCmdError : 0);
2333 MessageDisplay::create(event, alarm, mdFlags)->showDisplay();
2334 }
2335 else if (replaceReminder)
2336 {
2337 // The caption needs to be changed from "Reminder" to "Message"
2338 disp->cancelReminder(event, alarm);
2339 }
2340 else if (!disp->hasDefer() && event.repeatAtLogin() && !alarm.repeatAtLogin())
2341 {
2342 // It's a repeat-at-login message with no Defer button,
2343 // which has now reached its final trigger time and needs
2344 // to be replaced with a new message.
2345 disp->showDefer();
2346 disp->showDateTime(event, alarm);
2347 }
2348 else
2349 {
2350 // Use the existing message window
2351 }
2352 if (disp)
2353 {
2354 // Raise the existing message window and replay any sound
2355 disp->repeat(alarm); // N.B. this reschedules the alarm
2356 }
2357 break;
2358 }
2359 case KAAlarm::EMAIL:
2360 {
2361 qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: EMAIL to:" << event.emailAddresses(QStringLiteral(","));
2362 QStringList errmsgs;
2363 KAMail::JobData data(event, alarm, flags & Reschedule, flags & (Reschedule | AllowDefer));
2364 data.queued = true;
2365 int ans = KAMail::send(data, errmsgs);
2366 if (ans)
2367 {
2368 // The email has either been sent or failed - not queued
2369 if (ans < 0)
2370 result = nullptr; // failure
2371 data.queued = false;
2372 emailSent(data, errmsgs, (ans > 0));
2373 }
2374 else
2375 {
2376 result = (void*)-1; // email has been queued
2377 }
2378 if (flags & Reschedule)
2379 rescheduleAlarm(event, alarm, true);
2380 break;
2381 }
2382 case KAAlarm::AUDIO:
2383 {
2384 // Play the sound, provided that the same event
2385 // isn't already playing
2386 MessageDisplay* disp = MessageDisplay::findEvent(EventId(event));
2387 if (!disp)
2388 {
2389 // There isn't already a message for this event.
2390 const int mdFlags = (flags & Reschedule ? 0 : MessageDisplay::NoReschedule) | MessageDisplay::AlwaysHide;
2391 event.setNotify(false); // can't use notification system if audio only
2392 disp = MessageDisplay::create(event, alarm, mdFlags);
2393 }
2394 else
2395 {
2396 // There's an existing message window: replay the sound
2397 disp->repeat(alarm); // N.B. this reschedules the alarm
2398 }
2399 return dynamic_cast<MessageWindow*>(disp);
2400 }
2401 default:
2402 return nullptr;
2403 }
2404 return result;
2405 }
2406
2407 /******************************************************************************
2408 * Called when sending an email has completed.
2409 */
emailSent(KAMail::JobData & data,const QStringList & errmsgs,bool copyerr)2410 void KAlarmApp::emailSent(KAMail::JobData& data, const QStringList& errmsgs, bool copyerr)
2411 {
2412 if (!errmsgs.isEmpty())
2413 {
2414 // Some error occurred, although the email may have been sent successfully
2415 if (errmsgs.count() > 1)
2416 qCDebug(KALARM_LOG) << "KAlarmApp::emailSent:" << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1];
2417 MessageDisplay::showError(data.event, data.alarm.dateTime(), errmsgs);
2418 }
2419 else if (data.queued)
2420 Q_EMIT execAlarmSuccess();
2421 }
2422
2423 /******************************************************************************
2424 * Execute the command specified in a command alarm.
2425 * To connect to the output ready signals of the process, specify a slot to be
2426 * called by supplying 'receiver' and 'slot' parameters.
2427 */
execCommandAlarm(const KAEvent & event,const KAAlarm & alarm,bool noRecordError,QObject * receiver,const char * slotOutput,const char * methodExited)2428 ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, bool noRecordError,
2429 QObject* receiver, const char* slotOutput, const char* methodExited)
2430 {
2431 // doShellCommand() will error if the user is not authorised to run
2432 // shell commands.
2433 const int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0)
2434 | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0)
2435 | (noRecordError ? ProcData::NO_RECORD_ERROR : 0);
2436 const QString command = event.cleanText();
2437 if (event.commandScript())
2438 {
2439 // Store the command script in a temporary file for execution
2440 qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm: Script";
2441 const QString tmpfile = createTempScriptFile(command, false, event, alarm);
2442 if (tmpfile.isEmpty())
2443 {
2444 setEventCommandError(event, KAEvent::CMD_ERROR);
2445 return nullptr;
2446 }
2447 return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slotOutput, methodExited);
2448 }
2449 else
2450 {
2451 qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm:" << command;
2452 return doShellCommand(command, event, &alarm, flags, receiver, slotOutput, methodExited);
2453 }
2454 }
2455
2456 /******************************************************************************
2457 * Execute a shell command line specified by an alarm.
2458 * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via
2459 * execAlarm() once the command completes, the execAlarm() parameters being
2460 * derived from the remaining bits in 'flags'.
2461 * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is
2462 * a pre- or post-alarm action respectively.
2463 * To connect to the exited signal of the process, specify the name of a method
2464 * to be called by supplying 'receiver' and 'methodExited' parameters.
2465 * To connect to the output ready signals of the process, specify a slot to be
2466 * called by supplying 'receiver' and 'slotOutput' parameters.
2467 *
2468 * Note that if shell access is not authorised, the attempt to run the command
2469 * will be errored.
2470 *
2471 * Reply = process which has been started, or null if a process couldn't be started.
2472 */
doShellCommand(const QString & command,const KAEvent & event,const KAAlarm * alarm,int flags,QObject * receiver,const char * slotOutput,const char * methodExited)2473 ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QObject* receiver, const char* slotOutput, const char* methodExited)
2474 {
2475 qCDebug(KALARM_LOG) << "KAlarmApp::doShellCommand:" << command << "," << event.id();
2476 QIODevice::OpenMode mode = QIODevice::WriteOnly;
2477 QString cmd;
2478 QString tmpXtermFile;
2479 if (flags & ProcData::EXEC_IN_XTERM)
2480 {
2481 // Execute the command in a terminal window.
2482 cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile);
2483 if (cmd.isEmpty())
2484 {
2485 qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed (no terminal selected)";
2486 const QStringList errors{i18nc("@info", "Failed to execute command\n(no terminal selected for command alarms)")};
2487 commandErrorMsg(nullptr, event, alarm, flags, errors);
2488 return nullptr;
2489 }
2490 }
2491 else
2492 {
2493 cmd = command;
2494 mode = QIODevice::ReadWrite;
2495 }
2496
2497 ProcData* pd = nullptr;
2498 ShellProcess* proc = nullptr;
2499 if (!cmd.isEmpty())
2500 {
2501 // Use ShellProcess, which automatically checks whether the user is
2502 // authorised to run shell commands.
2503 proc = new ShellProcess(cmd);
2504 proc->setEnv(QStringLiteral("KALARM_UID"), event.id(), true);
2505 proc->setOutputChannelMode(KProcess::MergedChannels); // combine stdout & stderr
2506 connect(proc, &ShellProcess::shellExited, this, &KAlarmApp::slotCommandExited);
2507 if ((flags & ProcData::DISP_OUTPUT) && receiver && slotOutput)
2508 {
2509 connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slotOutput);
2510 connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slotOutput);
2511 }
2512 if (mode == QIODevice::ReadWrite && !event.logFile().isEmpty())
2513 {
2514 // Output is to be appended to a log file.
2515 // Set up a logging process to write the command's output to.
2516 QString heading;
2517 if (alarm && alarm->dateTime().isValid())
2518 {
2519 const QString dateTime = alarm->dateTime().formatLocale();
2520 heading = QStringLiteral("\n******* KAlarm %1 *******\n").arg(dateTime);
2521 }
2522 else
2523 heading = QStringLiteral("\n******* KAlarm *******\n");
2524 QFile logfile(event.logFile());
2525 if (logfile.open(QIODevice::Append | QIODevice::Text))
2526 {
2527 QTextStream out(&logfile);
2528 out << heading;
2529 logfile.close();
2530 }
2531 proc->setStandardOutputFile(event.logFile(), QIODevice::Append);
2532 }
2533 pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : nullptr), flags);
2534 if (flags & ProcData::TEMP_FILE)
2535 pd->tempFiles += command;
2536 if (!tmpXtermFile.isEmpty())
2537 pd->tempFiles += tmpXtermFile;
2538 if (receiver && methodExited)
2539 {
2540 pd->exitReceiver = receiver;
2541 pd->exitMethod = methodExited;
2542 }
2543 mCommandProcesses.append(pd);
2544 if (proc->start(mode))
2545 return proc;
2546 }
2547
2548 // Error executing command - report it
2549 qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed to start";
2550 commandErrorMsg(proc, event, alarm, flags);
2551 if (pd)
2552 {
2553 mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd));
2554 delete pd;
2555 }
2556 return nullptr;
2557 }
2558
2559 /******************************************************************************
2560 * Compose a command line to execute the given command in a terminal window.
2561 * 'tempScriptFile' receives the name of a temporary script file which is
2562 * invoked by the command line, if applicable.
2563 * Reply = command line, or empty string if error.
2564 */
composeXTermCommand(const QString & command,const KAEvent & event,const KAAlarm * alarm,int flags,QString & tempScriptFile) const2565 QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const
2566 {
2567 qCDebug(KALARM_LOG) << "KAlarmApp::composeXTermCommand:" << command << "," << event.id();
2568 tempScriptFile.clear();
2569 QString cmd = Preferences::cmdXTermCommand();
2570 if (cmd.isEmpty())
2571 return QString(); // no terminal application is configured
2572 cmd.replace(QLatin1String("%t"), KAboutData::applicationData().displayName()); // set the terminal window title
2573 if (cmd.indexOf(QLatin1String("%C")) >= 0)
2574 {
2575 // Execute the command from a temporary script file
2576 if (flags & ProcData::TEMP_FILE)
2577 cmd.replace(QLatin1String("%C"), command); // the command is already calling a temporary file
2578 else
2579 {
2580 tempScriptFile = createTempScriptFile(command, true, event, *alarm);
2581 if (tempScriptFile.isEmpty())
2582 return QString();
2583 cmd.replace(QLatin1String("%C"), tempScriptFile); // %C indicates where to insert the command
2584 }
2585 }
2586 else if (cmd.indexOf(QLatin1String("%W")) >= 0)
2587 {
2588 // Execute the command from a temporary script file,
2589 // with a sleep after the command is executed
2590 tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm);
2591 if (tempScriptFile.isEmpty())
2592 return QString();
2593 cmd.replace(QLatin1String("%W"), tempScriptFile); // %w indicates where to insert the command
2594 }
2595 else if (cmd.indexOf(QLatin1String("%w")) >= 0)
2596 {
2597 // Append a sleep to the command.
2598 // Quote the command in case it contains characters such as [>|;].
2599 const QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400"));
2600 cmd.replace(QLatin1String("%w"), exec); // %w indicates where to insert the command string
2601 }
2602 else
2603 {
2604 // Set the command to execute.
2605 // Put it in quotes in case it contains characters such as [>|;].
2606 const QString exec = KShell::quoteArg(command);
2607 if (cmd.indexOf(QLatin1String("%c")) >= 0)
2608 cmd.replace(QLatin1String("%c"), exec); // %c indicates where to insert the command string
2609 else
2610 cmd.append(exec); // otherwise, simply append the command string
2611 }
2612 return cmd;
2613 }
2614
2615 /******************************************************************************
2616 * Create a temporary script file containing the specified command string.
2617 * Reply = path of temporary file, or null string if error.
2618 */
createTempScriptFile(const QString & command,bool insertShell,const KAEvent & event,const KAAlarm & alarm) const2619 QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const
2620 {
2621 QTemporaryFile tmpFile;
2622 tmpFile.setAutoRemove(false); // don't delete file when it is destructed
2623 if (!tmpFile.open())
2624 qCCritical(KALARM_LOG) << "Unable to create a temporary script file";
2625 else
2626 {
2627 tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser);
2628 QTextStream stream(&tmpFile);
2629 if (insertShell)
2630 stream << "#!" << ShellProcess::shellPath() << "\n";
2631 stream << command;
2632 stream.flush();
2633 if (tmpFile.error() != QFile::NoError)
2634 qCCritical(KALARM_LOG) << "Error" << tmpFile.errorString() << " writing to temporary script file";
2635 else
2636 return tmpFile.fileName();
2637 }
2638
2639 const QStringList errmsgs(i18nc("@info", "Error creating temporary script file"));
2640 MessageDisplay::showError(event, alarm.dateTime(), errmsgs, QStringLiteral("Script"));
2641 return QString();
2642 }
2643
2644 /******************************************************************************
2645 * Called when a command alarm's execution completes.
2646 */
slotCommandExited(ShellProcess * proc)2647 void KAlarmApp::slotCommandExited(ShellProcess* proc)
2648 {
2649 qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited";
2650 // Find this command in the command list
2651 for (int i = 0, end = mCommandProcesses.count(); i < end; ++i)
2652 {
2653 ProcData* pd = mCommandProcesses.at(i);
2654 if (pd->process == proc)
2655 {
2656 // Found the command. Check its exit status.
2657 bool executeAlarm = pd->preAction();
2658 const ShellProcess::Status status = proc->status();
2659 if (status == ShellProcess::SUCCESS && !proc->exitCode())
2660 {
2661 qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": SUCCESS";
2662 clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE
2663 : pd->postAction() ? KAEvent::CMD_ERROR_POST
2664 : KAEvent::CMD_ERROR);
2665 }
2666 else
2667 {
2668 QString errmsg = proc->errorMessage();
2669 if (status == ShellProcess::SUCCESS || status == ShellProcess::NOT_FOUND)
2670 qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode();
2671 else
2672 qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status;
2673 if (pd->messageBoxParent)
2674 {
2675 // Close the existing informational KMessageBox for this process
2676 const QList<QDialog*> dialogs = pd->messageBoxParent->findChildren<QDialog*>();
2677 if (!dialogs.isEmpty())
2678 delete dialogs[0];
2679 setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE
2680 : pd->postAction() ? KAEvent::CMD_ERROR_POST
2681 : KAEvent::CMD_ERROR);
2682 if (!pd->tempFile())
2683 {
2684 errmsg += QLatin1Char('\n');
2685 errmsg += proc->command();
2686 }
2687 KAMessageBox::error(pd->messageBoxParent, errmsg);
2688 }
2689 else
2690 commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags);
2691
2692 if (executeAlarm
2693 && (pd->event->extraActionOptions() & KAEvent::CancelOnPreActError))
2694 {
2695 qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": pre-action failed: cancelled";
2696 if (pd->reschedule())
2697 rescheduleAlarm(*pd->event, *pd->alarm, true);
2698 executeAlarm = false;
2699 }
2700 }
2701 if (pd->preAction())
2702 ResourcesCalendar::setAlarmPending(*pd->event, false);
2703 if (executeAlarm)
2704 {
2705 execAlarm(*pd->event, *pd->alarm, (pd->reschedule() ? Reschedule : NoExecFlag)
2706 | (pd->allowDefer() ? AllowDefer : NoExecFlag)
2707 | (pd->noRecordCmdErr() ? NoRecordCmdError : NoExecFlag)
2708 | NoPreAction);
2709 }
2710 mCommandProcesses.removeAt(i);
2711 if (pd->exitReceiver && !pd->exitMethod.isEmpty())
2712 QMetaObject::invokeMethod(pd->exitReceiver, pd->exitMethod.constData(), Qt::DirectConnection, Q_ARG(ShellProcess::Status, status));
2713 delete pd;
2714 break;
2715 }
2716 }
2717
2718 // If there are now no executing shell commands, quit if a quit was queued
2719 if (mPendingQuit && mCommandProcesses.isEmpty())
2720 quitIf(mPendingQuitCode);
2721 }
2722
2723 /******************************************************************************
2724 * Output an error message for a shell command, and record the alarm's error status.
2725 */
commandErrorMsg(const ShellProcess * proc,const KAEvent & event,const KAAlarm * alarm,int flags,const QStringList & errors)2726 void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags, const QStringList& errors)
2727 {
2728 KAEvent::CmdErrType cmderr;
2729 QString dontShowAgain;
2730 QStringList errmsgs = errors;
2731 if (flags & ProcData::PRE_ACTION)
2732 {
2733 if (event.extraActionOptions() & KAEvent::DontShowPreActError)
2734 return; // don't notify user of any errors for the alarm
2735 errmsgs += i18nc("@info", "Pre-alarm action:");
2736 dontShowAgain = QStringLiteral("Pre");
2737 cmderr = KAEvent::CMD_ERROR_PRE;
2738 }
2739 else if (flags & ProcData::POST_ACTION)
2740 {
2741 errmsgs += i18nc("@info", "Post-alarm action:");
2742 dontShowAgain = QStringLiteral("Post");
2743 cmderr = (event.commandError() == KAEvent::CMD_ERROR_PRE)
2744 ? KAEvent::CMD_ERROR_PRE_POST : KAEvent::CMD_ERROR_POST;
2745 }
2746 else
2747 {
2748 if (!event.commandHideError())
2749 dontShowAgain = QStringLiteral("Exec");
2750 cmderr = KAEvent::CMD_ERROR;
2751 }
2752
2753 // Record the alarm's error status
2754 if (!(flags & ProcData::NO_RECORD_ERROR))
2755 setEventCommandError(event, cmderr);
2756
2757 if (!dontShowAgain.isEmpty())
2758 {
2759 // Display an error message
2760 if (proc)
2761 {
2762 errmsgs += proc->errorMessage();
2763 if (!(flags & ProcData::TEMP_FILE))
2764 errmsgs += proc->command();
2765 dontShowAgain += QString::number(proc->status());
2766 }
2767 MessageDisplay::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain);
2768 }
2769 }
2770
2771 /******************************************************************************
2772 * Notes that an informational KMessageBox is displayed for this process.
2773 */
commandMessage(ShellProcess * proc,QWidget * parent)2774 void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent)
2775 {
2776 // Find this command in the command list
2777 for (ProcData* pd : std::as_const(mCommandProcesses))
2778 {
2779 if (pd->process == proc)
2780 {
2781 pd->messageBoxParent = parent;
2782 break;
2783 }
2784 }
2785 }
2786
2787 /******************************************************************************
2788 * If this is the first time through, open the calendar file, and start
2789 * processing the execution queue.
2790 */
initCheck(bool calendarOnly)2791 bool KAlarmApp::initCheck(bool calendarOnly)
2792 {
2793 static bool firstTime = true;
2794 if (firstTime)
2795 qCDebug(KALARM_LOG) << "KAlarmApp::initCheck: first time";
2796
2797 if (initialiseTimerResources() || firstTime)
2798 {
2799 /* Need to open the display calendar now, since otherwise if display
2800 * alarms are immediately due, they will often be processed while
2801 * MessageDisplay::redisplayAlarms() is executing open() (but before
2802 * open() completes), which causes problems!!
2803 */
2804 DisplayCalendar::open();
2805 }
2806 if (firstTime)
2807 {
2808 setArchivePurgeDays();
2809
2810 firstTime = false;
2811 }
2812
2813 if (!calendarOnly)
2814 startProcessQueue(); // start processing the execution queue
2815
2816 return true;
2817 }
2818
2819 /******************************************************************************
2820 * Called when an audio thread starts or stops.
2821 */
notifyAudioPlaying(bool playing)2822 void KAlarmApp::notifyAudioPlaying(bool playing)
2823 {
2824 Q_EMIT audioPlaying(playing);
2825 }
2826
2827 /******************************************************************************
2828 * Stop audio play.
2829 */
stopAudio()2830 void KAlarmApp::stopAudio()
2831 {
2832 MessageDisplay::stopAudio();
2833 }
2834
2835 /******************************************************************************
2836 * Set the command error for the specified alarm.
2837 */
setEventCommandError(const KAEvent & event,KAEvent::CmdErrType err) const2838 void KAlarmApp::setEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const
2839 {
2840 ProcData* pd = findCommandProcess(event.id());
2841 if (pd && pd->eventDeleted)
2842 return; // the alarm has been deleted, so can't set error status
2843
2844 if (err == KAEvent::CMD_ERROR_POST && event.commandError() == KAEvent::CMD_ERROR_PRE)
2845 err = KAEvent::CMD_ERROR_PRE_POST;
2846 event.setCommandError(err);
2847 KAEvent ev = ResourcesCalendar::event(EventId(event));
2848 if (ev.isValid() && ev.commandError() != err)
2849 {
2850 ev.setCommandError(err);
2851 ResourcesCalendar::updateEvent(ev);
2852 }
2853 Resource resource = Resources::resourceForEvent(event.id());
2854 resource.handleCommandErrorChange(event);
2855 }
2856
2857 /******************************************************************************
2858 * Clear the command error for the specified alarm.
2859 */
clearEventCommandError(const KAEvent & event,KAEvent::CmdErrType err) const2860 void KAlarmApp::clearEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const
2861 {
2862 ProcData* pd = findCommandProcess(event.id());
2863 if (pd && pd->eventDeleted)
2864 return; // the alarm has been deleted, so can't set error status
2865
2866 auto newerr = static_cast<KAEvent::CmdErrType>(event.commandError() & ~err);
2867 event.setCommandError(newerr);
2868 KAEvent ev = ResourcesCalendar::event(EventId(event));
2869 if (ev.isValid())
2870 {
2871 newerr = static_cast<KAEvent::CmdErrType>(ev.commandError() & ~err);
2872 ev.setCommandError(newerr);
2873 ResourcesCalendar::updateEvent(ev);
2874 }
2875 Resource resource = Resources::resourceForEvent(event.id());
2876 resource.handleCommandErrorChange(event);
2877 }
2878
2879 /******************************************************************************
2880 * Find the currently executing command process for an event ID, if any.
2881 */
findCommandProcess(const QString & eventId) const2882 KAlarmApp::ProcData* KAlarmApp::findCommandProcess(const QString& eventId) const
2883 {
2884 for (ProcData* pd : std::as_const(mCommandProcesses))
2885 {
2886 if (pd->event->id() == eventId)
2887 return pd;
2888 }
2889 return nullptr;
2890 }
2891
2892
ProcData(ShellProcess * p,KAEvent * e,KAAlarm * a,int f)2893 KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f)
2894 : process(p)
2895 , event(e)
2896 , alarm(a)
2897 , flags(f)
2898 { }
2899
~ProcData()2900 KAlarmApp::ProcData::~ProcData()
2901 {
2902 while (!tempFiles.isEmpty())
2903 {
2904 // Delete the temporary file called by the XTerm command
2905 QFile f(tempFiles.constFirst());
2906 f.remove();
2907 tempFiles.removeFirst();
2908 }
2909 delete process;
2910 delete event;
2911 delete alarm;
2912 }
2913
2914 // vim: et sw=4:
2915