Move unittestenv to correct location
[kdepim.git] / kalarm / kalarmapp.cpp
blob544aaab1902f7b5025f21317242b04e0200b0bfc
1 /*
2 * kalarmapp.cpp - the KAlarm application object
3 * Program: kalarm
4 * Copyright © 2001-2016 by David Jarvie <djarvie@kde.org>
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 #include "kalarm.h"
22 #include "kalarmapp.h"
24 #include "alarmcalendar.h"
25 #include "alarmlistview.h"
26 #include "alarmtime.h"
27 #include "autoqpointer.h"
28 #include "commandoptions.h"
29 #include "dbushandler.h"
30 #include "editdlgtypes.h"
31 #include "collectionmodel.h"
32 #include "functions.h"
33 #include "kamail.h"
34 #include "mainwindow.h"
35 #include "messagebox.h"
36 #include "messagewin.h"
37 #include "preferences.h"
38 #include "prefdlg.h"
39 #include "shellprocess.h"
40 #include "startdaytimer.h"
41 #include "traywindow.h"
42 #include "kalarm_debug.h"
44 #include <kalarmcal/datetime.h>
45 #include <kalarmcal/karecurrence.h>
47 #include <KDBusService>
48 #include <KLocalizedString>
49 #include <kconfig.h>
50 #include <KConfigGui>
51 #include <KAboutData>
52 #include <KSharedConfig>
53 #include <kfileitem.h>
54 #include <kstandardguiitem.h>
55 #include <kservicetypetrader.h>
56 #include <netwm.h>
57 #include <kshell.h>
58 #include <ksystemtimezone.h>
60 #include <QObject>
61 #include <QTimer>
62 #include <QFile>
63 #include <QTextStream>
64 #include <QTemporaryFile>
65 #include <QtDBus/QtDBus>
66 #include <QStandardPaths>
67 #include <QSystemTrayIcon>
68 #include <QCommandLineParser>
70 #include <stdlib.h>
71 #include <ctype.h>
72 #include <iostream>
73 #include <climits>
75 static const int AKONADI_TIMEOUT = 30; // timeout (seconds) for Akonadi collections to be populated
77 static void setEventCommandError(const KAEvent&, KAEvent::CmdErrType);
78 static void clearEventCommandError(const KAEvent&, KAEvent::CmdErrType);
80 /******************************************************************************
81 * Find the maximum number of seconds late which a late-cancel alarm is allowed
82 * to be. This is calculated as the late cancel interval, plus a few seconds
83 * leeway to cater for any timing irregularities.
85 static inline int maxLateness(int lateCancel)
87 static const int LATENESS_LEEWAY = 5;
88 int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0;
89 return LATENESS_LEEWAY + lc;
93 KAlarmApp* KAlarmApp::mInstance = Q_NULLPTR;
94 int KAlarmApp::mActiveCount = 0;
95 int KAlarmApp::mFatalError = 0;
96 QString KAlarmApp::mFatalMessage;
99 /******************************************************************************
100 * Construct the application.
102 KAlarmApp::KAlarmApp(int& argc, char** argv)
103 : QApplication(argc, argv),
104 mInitialised(false),
105 mRedisplayAlarms(false),
106 mQuitting(false),
107 mReadOnly(false),
108 mLoginAlarmsDone(false),
109 mDBusHandler(new DBusHandler()),
110 mTrayWindow(Q_NULLPTR),
111 mAlarmTimer(Q_NULLPTR),
112 mArchivedPurgeDays(-1), // default to not purging
113 mPurgeDaysQueued(-1),
114 mPendingQuit(false),
115 mCancelRtcWake(false),
116 mProcessingQueue(false),
117 mAlarmsEnabled(true)
119 qCDebug(KALARM_LOG);
120 #ifndef NDEBUG
121 KAlarm::setTestModeConditions();
122 #endif
124 // Make this a unique application.
125 KDBusService* s = new KDBusService(KDBusService::Unique);
126 connect(this, &KAlarmApp::aboutToQuit, s, &KDBusService::deleteLater);
127 connect(s, &KDBusService::activateRequested, this, &KAlarmApp::activate);
129 setQuitOnLastWindowClosed(false);
130 Preferences::self(); // read KAlarm configuration
131 if (!Preferences::noAutoStart())
133 Preferences::setAutoStart(true);
134 Preferences::self()->save();
136 Preferences::connect(SIGNAL(startOfDayChanged(QTime)), this, SLOT(changeStartOfDay()));
137 Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotWorkTimeChanged(QTime,QTime,QBitArray)));
138 Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotHolidaysChanged(KHolidays::HolidayRegion)));
139 Preferences::connect(SIGNAL(feb29TypeChanged(Feb29Type)), this, SLOT(slotFeb29TypeChanged(Feb29Type)));
140 Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(slotShowInSystemTrayChanged()));
141 Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(setArchivePurgeDays()));
142 Preferences::connect(SIGNAL(messageFontChanged(QFont)), this, SLOT(slotMessageFontChanged(QFont)));
143 slotFeb29TypeChanged(Preferences::defaultFeb29Type());
145 KAEvent::setStartOfDay(Preferences::startOfDay());
146 KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd());
147 KAEvent::setHolidays(Preferences::holidays());
148 KAEvent::setDefaultFont(Preferences::messageFont());
149 if (initialise()) // initialise calendars and alarm timer
151 connect(AkonadiModel::instance(), &AkonadiModel::collectionAdded,
152 this, &KAlarmApp::purgeNewArchivedDefault);
153 connect(AkonadiModel::instance(), &Akonadi::EntityTreeModel::collectionTreeFetched,
154 this, &KAlarmApp::checkWritableCalendar);
155 connect(AkonadiModel::instance(), &AkonadiModel::migrationCompleted,
156 this, &KAlarmApp::checkWritableCalendar);
158 KConfigGroup config(KSharedConfig::openConfig(), "General");
159 mNoSystemTray = config.readEntry("NoSystemTray", false);
160 mOldShowInSystemTray = wantShowInSystemTray();
161 DateTime::setStartOfDay(Preferences::startOfDay());
162 mPrefsArchivedColour = Preferences::archivedColour();
165 // Check if KOrganizer is installed
166 const QString korg = QStringLiteral("korganizer");
167 mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty();
168 if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KOrganizer options disabled (KOrganizer not found)"; }
169 // Check if the window manager can't handle keyboard focus transfer between windows
170 mWindowFocusBroken = (QProcessEnvironment::systemEnvironment().value(QStringLiteral("XDG_CURRENT_DESKTOP")) == QLatin1String("Unity"));
171 if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "Window keyboard focus broken"; }
174 /******************************************************************************
176 KAlarmApp::~KAlarmApp()
178 while (!mCommandProcesses.isEmpty())
180 ProcData* pd = mCommandProcesses[0];
181 mCommandProcesses.pop_front();
182 delete pd;
184 AlarmCalendar::terminateCalendars();
187 /******************************************************************************
188 * Return the one and only KAlarmApp instance.
189 * If it doesn't already exist, it is created first.
191 KAlarmApp* KAlarmApp::create(int& argc, char** argv)
193 if (!mInstance)
195 mInstance = new KAlarmApp(argc, argv);
197 if (mFatalError)
198 mInstance->quitFatal();
200 return mInstance;
203 /******************************************************************************
204 * (Re)initialise things which are tidied up/closed by quitIf().
205 * Reinitialisation can be necessary if session restoration finds nothing to
206 * restore and starts quitting the application, but KAlarm then starts up again
207 * before the application has exited.
208 * Reply = true if calendars were initialised successfully,
209 * false if they were already initialised, or if initialisation failed.
211 bool KAlarmApp::initialise()
213 if (!mAlarmTimer)
215 mAlarmTimer = new QTimer(this);
216 mAlarmTimer->setSingleShot(true);
217 connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm);
219 if (!AlarmCalendar::resources())
221 qCDebug(KALARM_LOG) << "initialising calendars";
222 if (AlarmCalendar::initialiseCalendars())
224 connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm);
225 connect(AlarmCalendar::resources(), &AlarmCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded);
226 return true;
229 return false;
232 /******************************************************************************
233 * Restore the saved session if required.
235 bool KAlarmApp::restoreSession()
237 if (!isSessionRestored())
238 return false;
239 if (mFatalError)
241 quitFatal();
242 return false;
245 // Process is being restored by session management.
246 qCDebug(KALARM_LOG) << "Restoring";
247 ++mActiveCount;
248 // Create the session config object now.
249 // This is necessary since if initCheck() below causes calendars to be updated,
250 // the session config created after that points to an invalid file, resulting
251 // in no windows being restored followed by a later crash.
252 KConfigGui::sessionConfig();
254 // When KAlarm is session restored, automatically set start-at-login to true.
255 Preferences::self()->load();
256 Preferences::setAutoStart(true);
257 Preferences::setNoAutoStart(false);
258 Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression
259 Preferences::self()->save();
261 if (!initCheck(true)) // open the calendar file (needed for main windows), don't process queue yet
263 --mActiveCount;
264 quitIf(1, true); // error opening the main calendar - quit
265 return false;
267 MainWindow* trayParent = Q_NULLPTR;
268 for (int i = 1; KMainWindow::canBeRestored(i); ++i)
270 const QString type = KMainWindow::classNameOfToplevel(i);
271 if (type == QStringLiteral("MainWindow"))
273 MainWindow* win = MainWindow::create(true);
274 win->restore(i, false);
275 if (win->isHiddenTrayParent())
276 trayParent = win;
277 else
278 win->show();
280 else if (type == QStringLiteral("MessageWin"))
282 MessageWin* win = new MessageWin;
283 win->restore(i, false);
284 if (win->isValid())
286 if (AkonadiModel::instance()->isCollectionTreeFetched())
287 win->show();
289 else
290 delete win;
294 // Try to display the system tray icon if it is configured to be shown
295 if (trayParent || wantShowInSystemTray())
297 if (!MainWindow::count())
298 qCWarning(KALARM_LOG) << "no main window to be restored!?";
299 else
301 displayTrayIcon(true, trayParent);
302 // Occasionally for no obvious reason, the main main window is
303 // shown when it should be hidden, so hide it just to be sure.
304 if (trayParent)
305 trayParent->hide();
309 --mActiveCount;
310 if (quitIf(0)) // quit if no windows are open
311 return false; // quitIf() can sometimes return, despite calling exit()
313 // Check whether the KDE time zone daemon is running (but don't hold up initialisation)
314 QTimer::singleShot(0, this, &KAlarmApp::checkKtimezoned);
316 startProcessQueue(); // start processing the execution queue
317 return true;
320 /******************************************************************************
321 * Called for a unique QApplication when a new instance of the application is
322 * started.
324 void KAlarmApp::activate(const QStringList& args, const QString& workingDirectory)
326 Q_UNUSED(workingDirectory)
327 qCDebug(KALARM_LOG);
328 if (mFatalError)
330 quitFatal();
331 // return 1;
332 return;
335 // Parse and interpret command line arguments.
336 QCommandLineParser parser;
337 KAboutData::applicationData().setupCommandLine(&parser);
338 parser.setApplicationDescription(QApplication::applicationDisplayName());
339 const QStringList newArgs = CommandOptions::setOptions(&parser, args);
340 parser.process(newArgs);
341 KAboutData::applicationData().processCommandLine(&parser);
343 ++mActiveCount;
344 int exitCode = 0; // default = success
345 static bool firstInstance = true;
346 bool dontRedisplay = false;
347 if (!firstInstance || !isSessionRestored())
349 CommandOptions::process();
350 CommandOptions* options = CommandOptions::instance(); // fetch command line options
351 #ifndef NDEBUG
352 if (options->simulationTime().isValid())
353 KAlarm::setSimulatedSystemTime(options->simulationTime());
354 #endif
355 CommandOptions::Command command = options->command();
356 if (options->disableAll())
357 setAlarmsEnabled(false); // disable alarm monitoring
358 switch (command)
360 case CommandOptions::TRIGGER_EVENT:
361 case CommandOptions::CANCEL_EVENT:
363 // Display or delete the event with the specified event ID
364 EventFunc function = (command == CommandOptions::TRIGGER_EVENT) ? EVENT_TRIGGER : EVENT_CANCEL;
365 // Open the calendar, don't start processing execution queue yet,
366 // and wait for the Akonadi collection to be populated.
367 if (!initCheck(true, true, options->eventId().collectionId()))
368 exitCode = 1;
369 else
371 startProcessQueue(); // start processing the execution queue
372 dontRedisplay = true;
373 if (!handleEvent(options->eventId(), function, true))
375 CommandOptions::printError(xi18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not unique", QStringLiteral("--") + options->commandName(), options->eventId().eventId()));
376 exitCode = 1;
379 break;
381 case CommandOptions::LIST:
382 // Output a list of scheduled alarms to stdout.
383 // Open the calendar, don't start processing execution queue yet,
384 // and wait for all Akonadi collections to be populated.
385 mReadOnly = true; // don't need write access to calendars
386 if (!initCheck(true, true))
387 exitCode = 1;
388 else
390 dontRedisplay = true;
391 QStringList alarms = scheduledAlarmList();
392 for (int i = 0, count = alarms.count(); i < count; ++i)
393 std::cout << alarms[i].toUtf8().constData() << std::endl;
395 break;
396 case CommandOptions::EDIT:
397 // Edit a specified existing alarm.
398 // Open the calendar and wait for the Akonadi collection to be populated.
399 if (!initCheck(false, true, options->eventId().collectionId()))
400 exitCode = 1;
401 else if (!KAlarm::editAlarmById(options->eventId()))
403 CommandOptions::printError(xi18nc("@info:shell", "%1: Event <resource>%2</resource> not found, or not editable", QStringLiteral("--") + options->commandName(), options->eventId().eventId()));
404 exitCode = 1;
406 break;
408 case CommandOptions::EDIT_NEW:
410 // Edit a new alarm, and optionally preset selected values
411 if (!initCheck())
412 exitCode = 1;
413 else
415 // Use AutoQPointer to guard against crash on application exit while
416 // the dialogue is still open. It prevents double deletion (both on
417 // deletion of parent, and on return from this function).
418 AutoQPointer<EditAlarmDlg> editDlg = EditAlarmDlg::create(false, options->editType());
419 if (options->alarmTime().isValid())
420 editDlg->setTime(options->alarmTime());
421 if (options->recurrence())
422 editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount());
423 else if (options->flags() & KAEvent::REPEAT_AT_LOGIN)
424 editDlg->setRepeatAtLogin();
425 editDlg->setAction(options->editAction(), AlarmText(options->text()));
426 if (options->lateCancel())
427 editDlg->setLateCancel(options->lateCancel());
428 if (options->flags() & KAEvent::COPY_KORGANIZER)
429 editDlg->setShowInKOrganizer(true);
430 switch (options->editType())
432 case EditAlarmDlg::DISPLAY:
434 // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY
435 EditDisplayAlarmDlg* dlg = qobject_cast<EditDisplayAlarmDlg*>(editDlg);
436 if (options->fgColour().isValid())
437 dlg->setFgColour(options->fgColour());
438 if (options->bgColour().isValid())
439 dlg->setBgColour(options->bgColour());
440 if (!options->audioFile().isEmpty()
441 || options->flags() & (KAEvent::BEEP | KAEvent::SPEAK))
443 KAEvent::Flags flags = options->flags();
444 Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep
445 : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak
446 : Preferences::Sound_File;
447 dlg->setAudio(type, options->audioFile(), options->audioVolume(), (flags & KAEvent::REPEAT_SOUND ? 0 : -1));
449 if (options->reminderMinutes())
450 dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE));
451 if (options->flags() & KAEvent::CONFIRM_ACK)
452 dlg->setConfirmAck(true);
453 if (options->flags() & KAEvent::AUTO_CLOSE)
454 dlg->setAutoClose(true);
455 break;
457 case EditAlarmDlg::COMMAND:
458 break;
459 case EditAlarmDlg::EMAIL:
461 // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL
462 EditEmailAlarmDlg* dlg = qobject_cast<EditEmailAlarmDlg*>(editDlg);
463 if (options->fromID()
464 || !options->addressees().isEmpty()
465 || !options->subject().isEmpty()
466 || !options->attachments().isEmpty())
467 dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments());
468 if (options->flags() & KAEvent::EMAIL_BCC)
469 dlg->setBcc(true);
470 break;
472 case EditAlarmDlg::AUDIO:
474 // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO
475 EditAudioAlarmDlg* dlg = qobject_cast<EditAudioAlarmDlg*>(editDlg);
476 if (!options->audioFile().isEmpty() || options->audioVolume() >= 0)
477 dlg->setAudio(options->audioFile(), options->audioVolume());
478 break;
480 case EditAlarmDlg::NO_TYPE:
481 break;
483 KAlarm::execNewAlarmDlg(editDlg);
485 break;
487 case CommandOptions::EDIT_NEW_PRESET:
488 // Edit a new alarm, preset with a template
489 if (!initCheck())
490 exitCode = 1;
491 else
492 KAlarm::editNewAlarm(options->templateName());
493 break;
495 case CommandOptions::NEW:
496 // Display a message or file, execute a command, or send an email
497 if (!initCheck()
498 || !scheduleEvent(options->editAction(), options->text(), options->alarmTime(),
499 options->lateCancel(), options->flags(), options->bgColour(),
500 options->fgColour(), QFont(), options->audioFile(), options->audioVolume(),
501 options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()),
502 options->subRepeatInterval(), options->subRepeatCount(),
503 options->fromID(), options->addressees(),
504 options->subject(), options->attachments()))
505 exitCode = 1;
506 break;
508 case CommandOptions::TRAY:
509 // Display only the system tray icon
510 if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable())
512 if (!initCheck() // open the calendar, start processing execution queue
513 || !displayTrayIcon(true))
514 exitCode = 1;
515 break;
517 // fall through to NONE
518 case CommandOptions::NONE:
519 // No arguments - run interactively & display the main window
520 #ifndef NDEBUG
521 if (options->simulationTime().isValid() && !firstInstance)
522 break; // simulating time: don't open main window if already running
523 #endif
524 if (!initCheck())
525 exitCode = 1;
526 else
528 MainWindow* win = MainWindow::create();
529 if (command == CommandOptions::TRAY)
530 win->setWindowState(win->windowState() | Qt::WindowMinimized);
531 win->show();
533 break;
535 case CommandOptions::CMD_ERROR:
536 mReadOnly = true; // don't need write access to calendars
537 exitCode = 1;
538 break;
542 // If this is the first time through, redisplay any alarm message windows
543 // from last time.
544 if (firstInstance && !dontRedisplay && !exitCode)
546 /* First time through, so redisplay alarm message windows from last time.
547 * But it is possible for session restoration in some circumstances to
548 * not create any windows, in which case the alarm calendars will have
549 * been deleted - if so, don't try to do anything. (This has been known
550 * to happen under the Xfce desktop.)
552 if (AlarmCalendar::resources())
554 if (AkonadiModel::instance()->isCollectionTreeFetched())
556 mRedisplayAlarms = false;
557 MessageWin::redisplayAlarms();
559 else
560 mRedisplayAlarms = true;
564 --mActiveCount;
565 firstInstance = false;
567 // Quit the application if this was the last/only running "instance" of the program.
568 // Executing 'return' doesn't work very well since the program continues to
569 // run if no windows were created.
570 quitIf(exitCode);
572 // Check whether the KDE time zone daemon is running (but don't hold up initialisation)
573 QTimer::singleShot(0, this, &KAlarmApp::checkKtimezoned);
575 // return exitCode;
578 void KAlarmApp::checkKtimezoned()
580 // Check that the KDE time zone daemon is running
581 static bool done = false;
582 if (done)
583 return;
584 done = true;
585 if (!KSystemTimeZones::isTimeZoneDaemonAvailable())
587 qCDebug(KALARM_LOG) << "ktimezoned not running: using UTC only";
588 KAMessageBox::information(MainWindow::mainMainWindow(),
589 xi18nc("@info", "Time zones are not accessible:<nl/>KAlarm will use the UTC time zone.<nl/><nl/>(The KDE time zone service is not available:<nl/>check that <application>ktimezoned</application> is installed.)"),
590 QString(), QStringLiteral("tzunavailable"));
594 /******************************************************************************
595 * Quit the program, optionally only if there are no more "instances" running.
596 * Reply = true if program exited.
598 bool KAlarmApp::quitIf(int exitCode, bool force)
600 if (force)
602 // Quit regardless, except for message windows
603 mQuitting = true;
604 MainWindow::closeAll();
605 mQuitting = false;
606 displayTrayIcon(false);
607 if (MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms)
608 return false;
610 else if (mQuitting)
611 return false; // MainWindow::closeAll() causes quitIf() to be called again
612 else
614 // Quit only if there are no more "instances" running
615 mPendingQuit = false;
616 if (mActiveCount > 0 || MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms)
617 return false;
618 int mwcount = MainWindow::count();
619 MainWindow* mw = mwcount ? MainWindow::firstWindow() : Q_NULLPTR;
620 if (mwcount > 1 || (mwcount && (!mw->isHidden() || !mw->isTrayParent())))
621 return false;
622 // There are no windows left except perhaps a main window which is a hidden
623 // tray icon parent, or an always-hidden message window.
624 if (mTrayWindow)
626 // There is a system tray icon.
627 // Don't exit unless the system tray doesn't seem to exist.
628 if (checkSystemTray())
629 return false;
631 if (!mActionQueue.isEmpty() || !mCommandProcesses.isEmpty())
633 // Don't quit yet if there are outstanding actions on the execution queue
634 mPendingQuit = true;
635 mPendingQuitCode = exitCode;
636 return false;
640 // This was the last/only running "instance" of the program, so exit completely.
641 // NOTE: Everything which is terminated/deleted here must where applicable
642 // be initialised in the initialise() method, in case KAlarm is
643 // started again before application exit completes!
644 qCDebug(KALARM_LOG) << exitCode << ": quitting";
645 MessageWin::stopAudio(true);
646 if (mCancelRtcWake)
648 KAlarm::setRtcWakeTime(0, Q_NULLPTR);
649 KAlarm::deleteRtcWakeConfig();
651 delete mAlarmTimer; // prevent checking for alarms after deleting calendars
652 mAlarmTimer = Q_NULLPTR;
653 mInitialised = false; // prevent processQueue() from running
654 AlarmCalendar::terminateCalendars();
655 exit(exitCode);
656 return true; // sometimes we actually get to here, despite calling exit()
659 /******************************************************************************
660 * Called when the Quit menu item is selected.
661 * Closes the system tray window and all main windows, but does not exit the
662 * program if other windows are still open.
664 void KAlarmApp::doQuit(QWidget* parent)
666 qCDebug(KALARM_LOG);
667 if (KAMessageBox::warningCancelContinue(parent,
668 i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."),
669 QString(), KStandardGuiItem::quit(),
670 KStandardGuiItem::cancel(), Preferences::QUIT_WARN
671 ) != KMessageBox::Continue)
672 return;
673 if (!KAlarm::checkRtcWakeConfig(true).isEmpty())
675 // A wake-on-suspend alarm is set
676 if (KAMessageBox::warningCancelContinue(parent,
677 i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."),
678 QString(), KStandardGuiItem::quit()
679 ) != KMessageBox::Continue)
680 return;
681 mCancelRtcWake = true;
683 if (!Preferences::autoStart())
685 int option = KMessageBox::No;
686 if (!Preferences::autoStartChangedByUser())
688 option = KAMessageBox::questionYesNoCancel(parent,
689 xi18nc("@info", "Do you want to start KAlarm at login?<nl/>"
690 "(Note that alarms will be disabled if KAlarm is not started.)"),
691 QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(),
692 KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START);
694 switch (option)
696 case KMessageBox::Yes:
697 Preferences::setAutoStart(true);
698 Preferences::setNoAutoStart(false);
699 break;
700 case KMessageBox::No:
701 Preferences::setNoAutoStart(true);
702 break;
703 case KMessageBox::Cancel:
704 default:
705 return;
707 Preferences::self()->save();
709 quitIf(0, true);
712 /******************************************************************************
713 * Display an error message for a fatal error. Prevent further actions since
714 * the program state is unsafe.
716 void KAlarmApp::displayFatalError(const QString& message)
718 if (!mFatalError)
720 mFatalError = 1;
721 mFatalMessage = message;
722 if (mInstance)
723 QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal);
727 /******************************************************************************
728 * Quit the program, once the fatal error message has been acknowledged.
730 void KAlarmApp::quitFatal()
732 switch (mFatalError)
734 case 0:
735 case 2:
736 return;
737 case 1:
738 mFatalError = 2;
739 KMessageBox::error(Q_NULLPTR, mFatalMessage); // this is an application modal window
740 mFatalError = 3;
741 // fall through to '3'
742 case 3:
743 if (mInstance)
744 mInstance->quitIf(1, true);
745 break;
747 QTimer::singleShot(1000, this, &KAlarmApp::quitFatal);
750 /******************************************************************************
751 * Called by the alarm timer when the next alarm is due.
752 * Also called when the execution queue has finished processing to check for the
753 * next alarm.
755 void KAlarmApp::checkNextDueAlarm()
757 if (!mAlarmsEnabled)
758 return;
759 // Find the first alarm due
760 KAEvent* nextEvent = AlarmCalendar::resources()->earliestAlarm();
761 if (!nextEvent)
762 return; // there are no alarms pending
763 KDateTime nextDt = nextEvent->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime();
764 KDateTime now = KDateTime::currentDateTime(Preferences::timeZone());
765 qint64 interval = now.secsTo_long(nextDt);
766 qCDebug(KALARM_LOG) << "now:" << qPrintable(now.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", next:" << qPrintable(nextDt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", due:" << interval;
767 if (interval <= 0)
769 // Queue the alarm
770 queueAlarmId(*nextEvent);
771 qCDebug(KALARM_LOG) << nextEvent->id() << ": due now";
772 QTimer::singleShot(0, this, &KAlarmApp::processQueue);
774 else
776 // No alarm is due yet, so set timer to wake us when it's due.
777 // Check for integer overflow before setting timer.
778 #ifndef HIBERNATION_SIGNAL
779 /* TODO: REPLACE THIS CODE WHEN A SYSTEM NOTIFICATION SIGNAL BECOMES
780 * AVAILABLE FOR WAKEUP FROM HIBERNATION.
781 * Re-evaluate the next alarm time every minute, in case the
782 * system clock jumps. The most common case when the clock jumps
783 * is when a laptop wakes from hibernation. If timers were left to
784 * run, they would trigger late by the length of time the system
785 * was asleep.
787 if (interval > 60) // 1 minute
788 interval = 60;
789 #endif
790 interval *= 1000;
791 if (interval > INT_MAX)
792 interval = INT_MAX;
793 qCDebug(KALARM_LOG) << nextEvent->id() << "wait" << interval/1000 << "seconds";
794 mAlarmTimer->start(static_cast<int>(interval));
798 /******************************************************************************
799 * Called by the alarm timer when the next alarm is due.
800 * Also called when the execution queue has finished processing to check for the
801 * next alarm.
803 void KAlarmApp::queueAlarmId(const KAEvent& event)
805 EventId id(event);
806 for (int i = 0, end = mActionQueue.count(); i < end; ++i)
808 if (mActionQueue[i].function == EVENT_HANDLE && mActionQueue[i].eventId == id)
809 return; // the alarm is already queued
811 mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, id));
814 /******************************************************************************
815 * Start processing the execution queue.
817 void KAlarmApp::startProcessQueue()
819 if (!mInitialised)
821 qCDebug(KALARM_LOG);
822 mInitialised = true;
823 QTimer::singleShot(0, this, &KAlarmApp::processQueue); // process anything already queued
827 /******************************************************************************
828 * The main processing loop for KAlarm.
829 * All KAlarm operations involving opening or updating calendar files are called
830 * from this loop to ensure that only one operation is active at any one time.
831 * This precaution is necessary because KAlarm's activities are mostly
832 * asynchronous, being in response to D-Bus calls from other programs or timer
833 * events, any of which can be received in the middle of performing another
834 * operation. If a calendar file is opened or updated while another calendar
835 * operation is in progress, the program has been observed to hang, or the first
836 * calendar call has failed with data loss - clearly unacceptable!!
838 void KAlarmApp::processQueue()
840 if (mInitialised && !mProcessingQueue)
842 qCDebug(KALARM_LOG);
843 mProcessingQueue = true;
845 // Refresh alarms if that's been queued
846 KAlarm::refreshAlarmsIfQueued();
848 if (!mLoginAlarmsDone)
850 // Queue all at-login alarms once only, at program start-up.
851 // First, cancel any scheduled reminders or deferrals for them,
852 // since these will be superseded by the new at-login trigger.
853 KAEvent::List events = AlarmCalendar::resources()->atLoginAlarms();
854 for (int i = 0, end = events.count(); i < end; ++i)
856 KAEvent event = *events[i];
857 if (!cancelReminderAndDeferral(event))
859 if (mAlarmsEnabled)
860 queueAlarmId(event);
863 mLoginAlarmsDone = true;
866 // Process queued events
867 while (!mActionQueue.isEmpty())
869 ActionQEntry& entry = mActionQueue.head();
870 if (entry.eventId.isEmpty())
872 // It's a new alarm
873 switch (entry.function)
875 case EVENT_TRIGGER:
876 execAlarm(entry.event, entry.event.firstAlarm(), false);
877 break;
878 case EVENT_HANDLE:
879 KAlarm::addEvent(entry.event, Q_NULLPTR, Q_NULLPTR, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT);
880 break;
881 case EVENT_CANCEL:
882 break;
885 else
886 handleEvent(entry.eventId, entry.function);
887 mActionQueue.dequeue();
890 // Purge the default archived alarms resource if it's time to do so
891 if (mPurgeDaysQueued >= 0)
893 KAlarm::purgeArchive(mPurgeDaysQueued);
894 mPurgeDaysQueued = -1;
897 // Now that the queue has been processed, quit if a quit was queued
898 if (mPendingQuit)
900 if (quitIf(mPendingQuitCode))
901 return; // quitIf() can sometimes return, despite calling exit()
904 mProcessingQueue = false;
906 // Schedule the application to be woken when the next alarm is due
907 checkNextDueAlarm();
911 /******************************************************************************
912 * Called when a repeat-at-login alarm has been added externally.
913 * Queues the alarm for triggering.
914 * First, cancel any scheduled reminder or deferral for it, since these will be
915 * superseded by the new at-login trigger.
917 void KAlarmApp::atLoginEventAdded(const KAEvent& event)
919 KAEvent ev = event;
920 if (!cancelReminderAndDeferral(ev))
922 if (mAlarmsEnabled)
924 mActionQueue.enqueue(ActionQEntry(EVENT_HANDLE, EventId(ev)));
925 if (mInitialised)
926 QTimer::singleShot(0, this, &KAlarmApp::processQueue);
931 /******************************************************************************
932 * Called when the system tray main window is closed.
934 void KAlarmApp::removeWindow(TrayWindow*)
936 mTrayWindow = Q_NULLPTR;
939 /******************************************************************************
940 * Display or close the system tray icon.
942 bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent)
944 qCDebug(KALARM_LOG);
945 static bool creating = false;
946 if (show)
948 if (!mTrayWindow && !creating)
950 if (!QSystemTrayIcon::isSystemTrayAvailable())
951 return false;
952 if (!MainWindow::count())
954 // We have to have at least one main window to act
955 // as parent to the system tray icon (even if the
956 // window is hidden).
957 creating = true; // prevent main window constructor from creating an additional tray icon
958 parent = MainWindow::create();
959 creating = false;
961 mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow());
962 connect(mTrayWindow, &TrayWindow::deleted, this, &KAlarmApp::trayIconToggled);
963 Q_EMIT trayIconToggled();
965 if (!checkSystemTray())
966 quitIf(0); // exit the application if there are no open windows
969 else
971 delete mTrayWindow;
972 mTrayWindow = Q_NULLPTR;
974 return true;
977 /******************************************************************************
978 * Check whether the system tray icon has been housed in the system tray.
980 bool KAlarmApp::checkSystemTray()
982 if (!mTrayWindow)
983 return true;
984 if (QSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray)
986 qCDebug(KALARM_LOG) << "changed ->" << mNoSystemTray;
987 mNoSystemTray = !mNoSystemTray;
989 // Store the new setting in the config file, so that if KAlarm exits it will
990 // restart with the correct default.
991 KConfigGroup config(KSharedConfig::openConfig(), "General");
992 config.writeEntry("NoSystemTray", mNoSystemTray);
993 config.sync();
995 // Update other settings
996 slotShowInSystemTrayChanged();
998 return !mNoSystemTray;
1001 /******************************************************************************
1002 * Return the main window associated with the system tray icon.
1004 MainWindow* KAlarmApp::trayMainWindow() const
1006 return mTrayWindow ? mTrayWindow->assocMainWindow() : Q_NULLPTR;
1009 /******************************************************************************
1010 * Called when the show-in-system-tray preference setting has changed, to show
1011 * or hide the system tray icon.
1013 void KAlarmApp::slotShowInSystemTrayChanged()
1015 bool newShowInSysTray = wantShowInSystemTray();
1016 if (newShowInSysTray != mOldShowInSystemTray)
1018 // The system tray run mode has changed
1019 ++mActiveCount; // prevent the application from quitting
1020 MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : Q_NULLPTR;
1021 delete mTrayWindow; // remove the system tray icon if it is currently shown
1022 mTrayWindow = Q_NULLPTR;
1023 mOldShowInSystemTray = newShowInSysTray;
1024 if (newShowInSysTray)
1026 // Show the system tray icon
1027 displayTrayIcon(true);
1029 else
1031 // Stop showing the system tray icon
1032 if (win && win->isHidden())
1034 if (MainWindow::count() > 1)
1035 delete win;
1036 else
1038 win->setWindowState(win->windowState() | Qt::WindowMinimized);
1039 win->show();
1043 --mActiveCount;
1047 /******************************************************************************
1048 * Called when the start-of-day time preference setting has changed.
1049 * Change alarm times for date-only alarms.
1051 void KAlarmApp::changeStartOfDay()
1053 DateTime::setStartOfDay(Preferences::startOfDay());
1054 KAEvent::setStartOfDay(Preferences::startOfDay());
1055 AlarmCalendar::resources()->adjustStartOfDay();
1058 /******************************************************************************
1059 * Called when the default alarm message font preference setting has changed.
1060 * Notify KAEvent.
1062 void KAlarmApp::slotMessageFontChanged(const QFont& font)
1064 KAEvent::setDefaultFont(font);
1067 /******************************************************************************
1068 * Called when the working time preference settings have changed.
1069 * Notify KAEvent.
1071 void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days)
1073 KAEvent::setWorkTime(days, start, end);
1076 /******************************************************************************
1077 * Called when the holiday region preference setting has changed.
1078 * Notify KAEvent.
1080 void KAlarmApp::slotHolidaysChanged(const KHolidays::HolidayRegion& holidays)
1082 KAEvent::setHolidays(holidays);
1085 /******************************************************************************
1086 * Called when the date for February 29th recurrences has changed in the
1087 * preferences settings.
1089 void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type)
1091 KARecurrence::Feb29Type rtype;
1092 switch (type)
1094 default:
1095 case Preferences::Feb29_None: rtype = KARecurrence::Feb29_None; break;
1096 case Preferences::Feb29_Feb28: rtype = KARecurrence::Feb29_Feb28; break;
1097 case Preferences::Feb29_Mar1: rtype = KARecurrence::Feb29_Mar1; break;
1099 KARecurrence::setDefaultFeb29Type(rtype);
1102 /******************************************************************************
1103 * Return whether the program is configured to be running in the system tray.
1105 bool KAlarmApp::wantShowInSystemTray() const
1107 return Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable();
1110 /******************************************************************************
1111 * Called when all calendars have been fetched at startup.
1112 * Check whether there are any writable active calendars, and if not, warn the
1113 * user.
1115 void KAlarmApp::checkWritableCalendar()
1117 if (mReadOnly)
1118 return; // don't need write access to calendars
1119 bool treeFetched = AkonadiModel::instance()->isCollectionTreeFetched();
1120 if (treeFetched && mRedisplayAlarms)
1122 mRedisplayAlarms = false;
1123 MessageWin::redisplayAlarms();
1125 if (!treeFetched
1126 || !AkonadiModel::instance()->isMigrationCompleted())
1127 return;
1128 static bool done = false;
1129 if (done)
1130 return;
1131 done = true;
1132 qCDebug(KALARM_LOG);
1133 // Find whether there are any writable active alarm calendars
1134 bool active = !CollectionControlModel::enabledCollections(CalEvent::ACTIVE, true).isEmpty();
1135 if (!active)
1137 qCWarning(KALARM_LOG) << "No writable active calendar";
1138 KAMessageBox::information(MainWindow::mainMainWindow(),
1139 xi18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled.<nl/><nl/>"
1140 "To fix this, use <interface>View | Show Calendars</interface> to check or change calendar statuses."),
1141 QString(), QStringLiteral("noWritableCal"));
1145 /******************************************************************************
1146 * Called when a new collection has been added, or when a collection has been
1147 * set as the standard collection for its type.
1148 * If it is the default archived calendar, purge its old alarms if necessary.
1150 void KAlarmApp::purgeNewArchivedDefault(const Akonadi::Collection& collection)
1152 Akonadi::Collection col(collection);
1153 if (CollectionControlModel::isStandard(col, CalEvent::ARCHIVED))
1155 // Allow time (1 minute) for AkonadiModel to be populated with the
1156 // collection's events before purging it.
1157 qCDebug(KALARM_LOG) << collection.id() << ": standard archived...";
1158 QTimer::singleShot(60000, this, &KAlarmApp::purgeAfterDelay);
1162 /******************************************************************************
1163 * Called after a delay, after the default archived calendar has been added to
1164 * AkonadiModel.
1165 * Purge old alarms from it if necessary.
1167 void KAlarmApp::purgeAfterDelay()
1169 if (mArchivedPurgeDays >= 0)
1170 purge(mArchivedPurgeDays);
1171 else
1172 setArchivePurgeDays();
1175 /******************************************************************************
1176 * Called when the length of time to keep archived alarms changes in KAlarm's
1177 * preferences.
1178 * Set the number of days to keep archived alarms.
1179 * Alarms which are older are purged immediately, and at the start of each day.
1181 void KAlarmApp::setArchivePurgeDays()
1183 int newDays = Preferences::archivedKeepDays();
1184 if (newDays != mArchivedPurgeDays)
1186 int oldDays = mArchivedPurgeDays;
1187 mArchivedPurgeDays = newDays;
1188 if (mArchivedPurgeDays <= 0)
1189 StartOfDayTimer::disconnect(this);
1190 if (mArchivedPurgeDays < 0)
1191 return; // keep indefinitely, so don't purge
1192 if (oldDays < 0 || mArchivedPurgeDays < oldDays)
1194 // Alarms are now being kept for less long, so purge them
1195 purge(mArchivedPurgeDays);
1196 if (!mArchivedPurgeDays)
1197 return; // don't archive any alarms
1199 // Start the purge timer to expire at the start of the next day
1200 // (using the user-defined start-of-day time).
1201 StartOfDayTimer::connect(this, SLOT(slotPurge()));
1205 /******************************************************************************
1206 * Purge all archived events from the calendar whose end time is longer ago than
1207 * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero.
1209 void KAlarmApp::purge(int daysToKeep)
1211 if (mPurgeDaysQueued < 0 || daysToKeep < mPurgeDaysQueued)
1212 mPurgeDaysQueued = daysToKeep;
1214 // Do the purge once any other current operations are completed
1215 processQueue();
1219 /******************************************************************************
1220 * Output a list of pending alarms, with their next scheduled occurrence.
1222 QStringList KAlarmApp::scheduledAlarmList()
1224 QVector<KAEvent> events = KAlarm::getSortedActiveEvents(this);
1225 QStringList alarms;
1226 for (int i = 0, count = events.count(); i < count; ++i)
1228 KAEvent* event = &events[i];
1229 KDateTime dateTime = event->nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone();
1230 Akonadi::Collection c(event->collectionId());
1231 AkonadiModel::instance()->refresh(c);
1232 QString text(c.resource() + QLatin1String(":"));
1233 text += event->id() + QLatin1Char(' ')
1234 + dateTime.toString(QStringLiteral("%Y%m%dT%H%M "))
1235 + AlarmText::summary(*event, 1);
1236 alarms << text;
1238 return alarms;
1241 /******************************************************************************
1242 * Enable or disable alarm monitoring.
1244 void KAlarmApp::setAlarmsEnabled(bool enabled)
1246 if (enabled != mAlarmsEnabled)
1248 mAlarmsEnabled = enabled;
1249 Q_EMIT alarmEnabledToggled(enabled);
1250 if (!enabled)
1251 KAlarm::cancelRtcWake(Q_NULLPTR);
1252 else if (!mProcessingQueue)
1253 checkNextDueAlarm();
1257 /******************************************************************************
1258 * Spread or collect alarm message and error message windows.
1260 void KAlarmApp::spreadWindows(bool spread)
1262 spread = MessageWin::spread(spread);
1263 Q_EMIT spreadWindowsToggled(spread);
1266 /******************************************************************************
1267 * Called when the spread status of message windows changes.
1268 * Set the 'spread windows' action state.
1270 void KAlarmApp::setSpreadWindowsState(bool spread)
1272 Q_EMIT spreadWindowsToggled(spread);
1275 /******************************************************************************
1276 * Check whether the window manager's handling of keyboard focus transfer
1277 * between application windows is broken. This is true for Ubuntu's Unity
1278 * desktop, where MessageWin windows steal keyboard focus from EditAlarmDlg
1279 * windows.
1281 bool KAlarmApp::windowFocusBroken() const
1283 return mWindowFocusBroken;
1286 /******************************************************************************
1287 * Check whether window/keyboard focus currently needs to be fixed manually due
1288 * to the window manager not handling it correctly. This will occur if there are
1289 * both EditAlarmDlg and MessageWin windows currently active.
1291 bool KAlarmApp::needWindowFocusFix() const
1293 return mWindowFocusBroken && MessageWin::instanceCount(true) && EditAlarmDlg::instanceCount();
1296 /******************************************************************************
1297 * Called to schedule a new alarm, either in response to a DCOP notification or
1298 * to command line options.
1299 * Reply = true unless there was a parameter error or an error opening calendar file.
1301 bool KAlarmApp::scheduleEvent(KAEvent::SubAction action, const QString& text, const KDateTime& dateTime,
1302 int lateCancel, KAEvent::Flags flags, const QColor& bg, const QColor& fg,
1303 const QFont& font, const QString& audioFile, float audioVolume, int reminderMinutes,
1304 const KARecurrence& recurrence, KCalCore::Duration repeatInterval, int repeatCount,
1305 uint mailFromID, const KCalCore::Person::List& mailAddresses,
1306 const QString& mailSubject, const QStringList& mailAttachments)
1308 qCDebug(KALARM_LOG) << text;
1309 if (!dateTime.isValid())
1310 return false;
1311 KDateTime now = KDateTime::currentUtcDateTime();
1312 if (lateCancel && dateTime < now.addSecs(-maxLateness(lateCancel)))
1313 return true; // alarm time was already archived too long ago
1314 KDateTime alarmTime = dateTime;
1315 // Round down to the nearest minute to avoid scheduling being messed up
1316 if (!dateTime.isDateOnly())
1317 alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0));
1319 KAEvent event(alarmTime, text, bg, fg, font, action, lateCancel, flags, true);
1320 if (reminderMinutes)
1322 bool onceOnly = flags & KAEvent::REMINDER_ONCE;
1323 event.setReminder(reminderMinutes, onceOnly);
1325 if (!audioFile.isEmpty())
1326 event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1);
1327 if (!mailAddresses.isEmpty())
1328 event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments);
1329 event.setRecurrence(recurrence);
1330 event.setFirstRecurrence();
1331 event.setRepetition(Repetition(repeatInterval, repeatCount - 1));
1332 event.endChanges();
1333 if (alarmTime <= now)
1335 // Alarm is due for display already.
1336 // First execute it once without adding it to the calendar file.
1337 if (!mInitialised)
1338 mActionQueue.enqueue(ActionQEntry(event, EVENT_TRIGGER));
1339 else
1340 execAlarm(event, event.firstAlarm(), false);
1341 // If it's a recurring alarm, reschedule it for its next occurrence
1342 if (!event.recurs()
1343 || event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE)
1344 return true;
1345 // It has recurrences in the future
1348 // Queue the alarm for insertion into the calendar file
1349 mActionQueue.enqueue(ActionQEntry(event));
1350 if (mInitialised)
1351 QTimer::singleShot(0, this, &KAlarmApp::processQueue);
1352 return true;
1355 /******************************************************************************
1356 * Called in response to a D-Bus request to trigger or cancel an event.
1357 * Optionally display the event. Delete the event from the calendar file and
1358 * from every main window instance.
1360 bool KAlarmApp::dbusHandleEvent(const EventId& eventID, EventFunc function)
1362 qCDebug(KALARM_LOG) << eventID;
1363 mActionQueue.append(ActionQEntry(function, eventID));
1364 if (mInitialised)
1365 QTimer::singleShot(0, this, &KAlarmApp::processQueue);
1366 return true;
1369 /******************************************************************************
1370 * Called in response to a D-Bus request to list all pending alarms.
1372 QString KAlarmApp::dbusList()
1374 qCDebug(KALARM_LOG);
1375 return scheduledAlarmList().join(QStringLiteral("\n")) + QLatin1Char('\n');
1378 /******************************************************************************
1379 * Either:
1380 * a) Display the event and then delete it if it has no outstanding repetitions.
1381 * b) Delete the event.
1382 * c) Reschedule the event for its next repetition. If none remain, delete it.
1383 * If the event is deleted, it is removed from the calendar file and from every
1384 * main window instance.
1385 * Reply = false if event ID not found, or if more than one event with the same
1386 * ID is found.
1388 bool KAlarmApp::handleEvent(const EventId& id, EventFunc function, bool checkDuplicates)
1390 // Delete any expired wake-on-suspend config data
1391 KAlarm::checkRtcWakeConfig();
1393 const QString eventID(id.eventId());
1394 KAEvent* event = AlarmCalendar::resources()->event(id, checkDuplicates);
1395 if (!event)
1397 if (id.collectionId() != -1)
1398 qCWarning(KALARM_LOG) << "Event ID not found, or duplicated:" << eventID;
1399 else
1400 qCWarning(KALARM_LOG) << "Event ID not found:" << eventID;
1401 return false;
1403 switch (function)
1405 case EVENT_CANCEL:
1406 qCDebug(KALARM_LOG) << eventID << ", CANCEL";
1407 KAlarm::deleteEvent(*event, true);
1408 break;
1410 case EVENT_TRIGGER: // handle it if it's due, else execute it regardless
1411 case EVENT_HANDLE: // handle it if it's due
1413 KDateTime now = KDateTime::currentUtcDateTime();
1414 qCDebug(KALARM_LOG) << eventID << "," << (function==EVENT_TRIGGER?"TRIGGER:":"HANDLE:") << qPrintable(now.dateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC";
1415 bool updateCalAndDisplay = false;
1416 bool alarmToExecuteValid = false;
1417 KAAlarm alarmToExecute;
1418 bool restart = false;
1419 // Check all the alarms in turn.
1420 // Note that the main alarm is fetched before any other alarms.
1421 for (KAAlarm alarm = event->firstAlarm();
1422 alarm.isValid();
1423 alarm = (restart ? event->firstAlarm() : event->nextAlarm(alarm)), restart = false)
1425 // Check if the alarm is due yet.
1426 KDateTime nextDT = alarm.dateTime(true).effectiveKDateTime();
1427 int secs = nextDT.secsTo(now);
1428 if (secs < 0)
1430 // The alarm appears to be in the future.
1431 // Check if it's an invalid local clock time during a daylight
1432 // saving time shift, which has actually passed.
1433 if (alarm.dateTime().timeSpec() != KDateTime::ClockTime
1434 || nextDT > now.toTimeSpec(KDateTime::ClockTime))
1436 // This alarm is definitely not due yet
1437 qCDebug(KALARM_LOG) << "Alarm" << alarm.type() << "at" << nextDT.dateTime() << ": not due";
1438 continue;
1441 bool reschedule = false;
1442 bool rescheduleWork = false;
1443 if ((event->workTimeOnly() || event->holidaysExcluded()) && !alarm.deferred())
1445 // The alarm is restricted to working hours and/or non-holidays
1446 // (apart from deferrals). This needs to be re-evaluated every
1447 // time it triggers, since working hours could change.
1448 if (alarm.dateTime().isDateOnly())
1450 KDateTime dt(nextDT);
1451 dt.setDateOnly(true);
1452 reschedule = !event->isWorkingTime(dt);
1454 else
1455 reschedule = !event->isWorkingTime(nextDT);
1456 rescheduleWork = reschedule;
1457 if (reschedule)
1458 qCDebug(KALARM_LOG) << "Alarm" << alarm.type() << "at" << nextDT.dateTime() << ": not during working hours";
1460 if (!reschedule && alarm.repeatAtLogin())
1462 // Alarm is to be displayed at every login.
1463 qCDebug(KALARM_LOG) << "REPEAT_AT_LOGIN";
1464 // Check if the main alarm is already being displayed.
1465 // (We don't want to display both at the same time.)
1466 if (alarmToExecute.isValid())
1467 continue;
1469 // Set the time to display if it's a display alarm
1470 alarm.setTime(now);
1472 if (!reschedule && event->lateCancel())
1474 // Alarm is due, and it is to be cancelled if too late.
1475 qCDebug(KALARM_LOG) << "LATE_CANCEL";
1476 bool cancel = false;
1477 if (alarm.dateTime().isDateOnly())
1479 // The alarm has no time, so cancel it if its date is too far past
1480 int maxlate = event->lateCancel() / 1440; // maximum lateness in days
1481 KDateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime());
1482 if (now >= limit)
1484 // It's too late to display the scheduled occurrence.
1485 // Find the last previous occurrence of the alarm.
1486 DateTime next;
1487 KAEvent::OccurType type = event->previousOccurrence(now, next, true);
1488 switch (type & ~KAEvent::OCCURRENCE_REPEAT)
1490 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
1491 case KAEvent::RECURRENCE_DATE:
1492 case KAEvent::RECURRENCE_DATE_TIME:
1493 case KAEvent::LAST_RECURRENCE:
1494 limit.setDate(next.date().addDays(maxlate + 1));
1495 if (now >= limit)
1497 if (type == KAEvent::LAST_RECURRENCE
1498 || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs()))
1499 cancel = true; // last occurrence (and there are no repetitions)
1500 else
1501 reschedule = true;
1503 break;
1504 case KAEvent::NO_OCCURRENCE:
1505 default:
1506 reschedule = true;
1507 break;
1511 else
1513 // The alarm is timed. Allow it to be the permitted amount late before cancelling it.
1514 int maxlate = maxLateness(event->lateCancel());
1515 if (secs > maxlate)
1517 // It's over the maximum interval late.
1518 // Find the most recent occurrence of the alarm.
1519 DateTime next;
1520 KAEvent::OccurType type = event->previousOccurrence(now, next, true);
1521 switch (type & ~KAEvent::OCCURRENCE_REPEAT)
1523 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
1524 case KAEvent::RECURRENCE_DATE:
1525 case KAEvent::RECURRENCE_DATE_TIME:
1526 case KAEvent::LAST_RECURRENCE:
1527 if (next.effectiveKDateTime().secsTo(now) > maxlate)
1529 if (type == KAEvent::LAST_RECURRENCE
1530 || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs()))
1531 cancel = true; // last occurrence (and there are no repetitions)
1532 else
1533 reschedule = true;
1535 break;
1536 case KAEvent::NO_OCCURRENCE:
1537 default:
1538 reschedule = true;
1539 break;
1544 if (cancel)
1546 // All recurrences are finished, so cancel the event
1547 event->setArchive();
1548 if (cancelAlarm(*event, alarm.type(), false))
1549 return true; // event has been deleted
1550 updateCalAndDisplay = true;
1551 continue;
1554 if (reschedule)
1556 // The latest repetition was too long ago, so schedule the next one
1557 switch (rescheduleAlarm(*event, alarm, false, (rescheduleWork ? nextDT : KDateTime())))
1559 case 1:
1560 // A working-time-only alarm has been rescheduled and the
1561 // rescheduled time is already due. Start processing the
1562 // event again.
1563 alarmToExecuteValid = false;
1564 restart = true;
1565 break;
1566 case -1:
1567 return true; // event has been deleted
1568 default:
1569 break;
1571 updateCalAndDisplay = true;
1572 continue;
1574 if (!alarmToExecuteValid)
1576 qCDebug(KALARM_LOG) << "Alarm" << alarm.type() << ": execute";
1577 alarmToExecute = alarm; // note the alarm to be displayed
1578 alarmToExecuteValid = true; // only trigger one alarm for the event
1580 else
1581 qCDebug(KALARM_LOG) << "Alarm" << alarm.type() << ": skip";
1584 // If there is an alarm to execute, do this last after rescheduling/cancelling
1585 // any others. This ensures that the updated event is only saved once to the calendar.
1586 if (alarmToExecute.isValid())
1587 execAlarm(*event, alarmToExecute, true, !alarmToExecute.repeatAtLogin());
1588 else
1590 if (function == EVENT_TRIGGER)
1592 // The alarm is to be executed regardless of whether it's due.
1593 // Only trigger one alarm from the event - we don't want multiple
1594 // identical messages, for example.
1595 KAAlarm alarm = event->firstAlarm();
1596 if (alarm.isValid())
1597 execAlarm(*event, alarm, false);
1599 if (updateCalAndDisplay)
1600 KAlarm::updateEvent(*event); // update the window lists and calendar file
1601 else if (function != EVENT_TRIGGER) { qCDebug(KALARM_LOG) << "No action"; }
1603 break;
1606 return true;
1609 /******************************************************************************
1610 * Called when an alarm action has completed, to perform any post-alarm actions.
1612 void KAlarmApp::alarmCompleted(const KAEvent& event)
1614 if (!event.postAction().isEmpty())
1616 // doShellCommand() will error if the user is not authorised to run
1617 // shell commands.
1618 QString command = event.postAction();
1619 qCDebug(KALARM_LOG) << event.id() << ":" << command;
1620 doShellCommand(command, event, Q_NULLPTR, ProcData::POST_ACTION);
1624 /******************************************************************************
1625 * Reschedule the alarm for its next recurrence after now. If none remain,
1626 * delete it. If the alarm is deleted and it is the last alarm for its event,
1627 * the event is removed from the calendar file and from every main window
1628 * instance.
1629 * If 'nextDt' is valid, the event is rescheduled for the next non-working
1630 * time occurrence after that.
1631 * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due
1632 * = -1 if the event has been deleted
1633 * = 0 otherwise.
1635 int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KDateTime& nextDt)
1637 qCDebug(KALARM_LOG) << "Alarm type:" << alarm.type();
1638 int reply = 0;
1639 bool update = false;
1640 event.startChanges();
1641 if (alarm.repeatAtLogin())
1643 // Leave an alarm which repeats at every login until its main alarm triggers
1644 if (!event.reminderActive() && event.reminderMinutes() < 0)
1646 // Executing an at-login alarm: first schedule the reminder
1647 // which occurs AFTER the main alarm.
1648 event.activateReminderAfter(KDateTime::currentUtcDateTime());
1649 update = true;
1652 else if (alarm.isReminder() || alarm.deferred())
1654 // It's a reminder alarm or an extra deferred alarm, so delete it
1655 event.removeExpiredAlarm(alarm.type());
1656 update = true;
1658 else
1660 // Reschedule the alarm for its next occurrence.
1661 bool cancelled = false;
1662 DateTime last = event.mainDateTime(false); // note this trigger time
1663 if (last != event.mainDateTime(true))
1664 last = DateTime(); // but ignore sub-repetition triggers
1665 bool next = nextDt.isValid();
1666 KDateTime next_dt = nextDt;
1667 KDateTime now = KDateTime::currentUtcDateTime();
1670 KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now);
1671 switch (type)
1673 case KAEvent::NO_OCCURRENCE:
1674 // All repetitions are finished, so cancel the event
1675 qCDebug(KALARM_LOG) << "No occurrence";
1676 if (event.reminderMinutes() < 0 && last.isValid()
1677 && alarm.type() != KAAlarm::AT_LOGIN_ALARM && !event.mainExpired())
1679 // Set the reminder which is now due after the last main alarm trigger.
1680 // Note that at-login reminders are scheduled in execAlarm().
1681 event.activateReminderAfter(last);
1682 updateCalAndDisplay = true;
1684 if (cancelAlarm(event, alarm.type(), updateCalAndDisplay))
1685 return -1;
1686 break;
1687 default:
1688 if (!(type & KAEvent::OCCURRENCE_REPEAT))
1689 break;
1690 // Next occurrence is a repeat, so fall through to recurrence handling
1691 case KAEvent::RECURRENCE_DATE:
1692 case KAEvent::RECURRENCE_DATE_TIME:
1693 case KAEvent::LAST_RECURRENCE:
1694 // The event is due by now and repetitions still remain, so rewrite the event
1695 if (updateCalAndDisplay)
1696 update = true;
1697 break;
1698 case KAEvent::FIRST_OR_ONLY_OCCURRENCE:
1699 // The first occurrence is still due?!?, so don't do anything
1700 break;
1702 if (cancelled)
1703 break;
1704 if (event.deferred())
1706 // Just in case there's also a deferred alarm, ensure it's removed
1707 event.removeExpiredAlarm(KAAlarm::DEFERRED_ALARM);
1708 update = true;
1710 if (next)
1712 // The alarm is restricted to working hours and/or non-holidays.
1713 // Check if the calculated next time is valid.
1714 next_dt = event.mainDateTime(true).effectiveKDateTime();
1715 if (event.mainDateTime(false).isDateOnly())
1717 KDateTime dt(next_dt);
1718 dt.setDateOnly(true);
1719 next = !event.isWorkingTime(dt);
1721 else
1722 next = !event.isWorkingTime(next_dt);
1724 } while (next && next_dt <= now);
1725 reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0;
1727 if (event.reminderMinutes() < 0 && last.isValid()
1728 && alarm.type() != KAAlarm::AT_LOGIN_ALARM)
1730 // Set the reminder which is now due after the last main alarm trigger.
1731 // Note that at-login reminders are scheduled in execAlarm().
1732 event.activateReminderAfter(last);
1735 event.endChanges();
1736 if (update)
1737 KAlarm::updateEvent(event); // update the window lists and calendar file
1738 return reply;
1741 /******************************************************************************
1742 * Delete the alarm. If it is the last alarm for its event, the event is removed
1743 * from the calendar file and from every main window instance.
1744 * Reply = true if event has been deleted.
1746 bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay)
1748 qCDebug(KALARM_LOG);
1749 if (alarmType == KAAlarm::MAIN_ALARM && !event.displaying() && event.toBeArchived())
1751 // The event is being deleted. Save it in the archived resources first.
1752 KAEvent ev(event);
1753 KAlarm::addArchivedEvent(ev);
1755 event.removeExpiredAlarm(alarmType);
1756 if (!event.alarmCount())
1758 KAlarm::deleteEvent(event, false);
1759 return true;
1761 if (updateCalAndDisplay)
1762 KAlarm::updateEvent(event); // update the window lists and calendar file
1763 return false;
1766 /******************************************************************************
1767 * Cancel any reminder or deferred alarms in an repeat-at-login event.
1768 * This should be called when the event is first loaded.
1769 * If there are no more alarms left in the event, the event is removed from the
1770 * calendar file and from every main window instance.
1771 * Reply = true if event has been deleted.
1773 bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event)
1775 return cancelAlarm(event, KAAlarm::REMINDER_ALARM, false)
1776 || cancelAlarm(event, KAAlarm::DEFERRED_REMINDER_ALARM, false)
1777 || cancelAlarm(event, KAAlarm::DEFERRED_ALARM, true);
1780 /******************************************************************************
1781 * Execute an alarm by displaying its message or file, or executing its command.
1782 * Reply = ShellProcess instance if a command alarm
1783 * = MessageWin if an audio alarm
1784 * != 0 if successful
1785 * = -1 if execution has not completed
1786 * = 0 if the alarm is disabled, or if an error message was output.
1788 void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, bool reschedule, bool allowDefer, bool noPreAction)
1790 if (!mAlarmsEnabled || !event.enabled())
1792 // The event (or all events) is disabled
1793 qCDebug(KALARM_LOG) << event.id() << ": disabled";
1794 if (reschedule)
1795 rescheduleAlarm(event, alarm, true);
1796 return Q_NULLPTR;
1799 void* result = (void*)1;
1800 event.setArchive();
1802 switch (alarm.action())
1804 case KAAlarm::COMMAND:
1805 if (!event.commandDisplay())
1807 // execCommandAlarm() will error if the user is not authorised
1808 // to run shell commands.
1809 result = execCommandAlarm(event, alarm);
1810 if (reschedule)
1811 rescheduleAlarm(event, alarm, true);
1812 break;
1814 // fall through to MESSAGE
1815 case KAAlarm::MESSAGE:
1816 case KAAlarm::FILE:
1818 // Display a message, file or command output, provided that the same event
1819 // isn't already being displayed
1820 MessageWin* win = MessageWin::findEvent(EventId(event));
1821 // Find if we're changing a reminder message to the real message
1822 bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM);
1823 bool replaceReminder = !reminder && win && (win->alarmType() & KAAlarm::REMINDER_ALARM);
1824 if (!reminder
1825 && (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral))
1826 && (replaceReminder || !win) && !noPreAction
1827 && !event.preAction().isEmpty())
1829 // It's not a reminder alarm, and it's not a deferred alarm unless the
1830 // pre-alarm action applies to deferred alarms, and there is no message
1831 // window (other than a reminder window) currently displayed for this
1832 // alarm, and we need to execute a command before displaying the new window.
1834 // NOTE: The pre-action is not executed for a recurring alarm if an
1835 // alarm message window for a previous occurrence is still visible.
1836 // Check whether the command is already being executed for this alarm.
1837 for (int i = 0, end = mCommandProcesses.count(); i < end; ++i)
1839 ProcData* pd = mCommandProcesses[i];
1840 if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION))
1842 qCDebug(KALARM_LOG) << "Already executing pre-DISPLAY command";
1843 return pd->process; // already executing - don't duplicate the action
1847 // doShellCommand() will error if the user is not authorised to run
1848 // shell commands.
1849 QString command = event.preAction();
1850 qCDebug(KALARM_LOG) << "Pre-DISPLAY command:" << command;
1851 int flags = (reschedule ? ProcData::RESCHEDULE : 0) | (allowDefer ? ProcData::ALLOW_DEFER : 0);
1852 if (doShellCommand(command, event, &alarm, (flags | ProcData::PRE_ACTION)))
1854 AlarmCalendar::resources()->setAlarmPending(&event);
1855 return result; // display the message after the command completes
1857 // Error executing command
1858 if (event.extraActionOptions() & KAEvent::CancelOnPreActError)
1860 // Cancel the rest of the alarm execution
1861 qCDebug(KALARM_LOG) << event.id() << ": pre-action failed: cancelled";
1862 if (reschedule)
1863 rescheduleAlarm(event, alarm, true);
1864 return Q_NULLPTR;
1866 // Display the message even though it failed
1869 if (!win)
1871 // There isn't already a message for this event
1872 int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | (allowDefer ? 0 : MessageWin::NO_DEFER);
1873 (new MessageWin(&event, alarm, flags))->show();
1875 else if (replaceReminder)
1877 // The caption needs to be changed from "Reminder" to "Message"
1878 win->cancelReminder(event, alarm);
1880 else if (!win->hasDefer() && !alarm.repeatAtLogin())
1882 // It's a repeat-at-login message with no Defer button,
1883 // which has now reached its final trigger time and needs
1884 // to be replaced with a new message.
1885 win->showDefer();
1886 win->showDateTime(event, alarm);
1888 else
1890 // Use the existing message window
1892 if (win)
1894 // Raise the existing message window and replay any sound
1895 win->repeat(alarm); // N.B. this reschedules the alarm
1897 break;
1899 case KAAlarm::EMAIL:
1901 qCDebug(KALARM_LOG) << "EMAIL to:" << event.emailAddresses(QStringLiteral(","));
1902 QStringList errmsgs;
1903 KAMail::JobData data(event, alarm, reschedule, (reschedule || allowDefer));
1904 data.queued = true;
1905 int ans = KAMail::send(data, errmsgs);
1906 if (ans)
1908 // The email has either been sent or failed - not queued
1909 if (ans < 0)
1910 result = Q_NULLPTR; // failure
1911 data.queued = false;
1912 emailSent(data, errmsgs, (ans > 0));
1914 else
1916 result = (void*)-1; // email has been queued
1918 if (reschedule)
1919 rescheduleAlarm(event, alarm, true);
1920 break;
1922 case KAAlarm::AUDIO:
1924 // Play the sound, provided that the same event
1925 // isn't already playing
1926 MessageWin* win = MessageWin::findEvent(EventId(event));
1927 if (!win)
1929 // There isn't already a message for this event.
1930 int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | MessageWin::ALWAYS_HIDE;
1931 win = new MessageWin(&event, alarm, flags);
1933 else
1935 // There's an existing message window: replay the sound
1936 win->repeat(alarm); // N.B. this reschedules the alarm
1938 return win;
1940 default:
1941 return Q_NULLPTR;
1943 return result;
1946 /******************************************************************************
1947 * Called when sending an email has completed.
1949 void KAlarmApp::emailSent(KAMail::JobData& data, const QStringList& errmsgs, bool copyerr)
1951 if (!errmsgs.isEmpty())
1953 // Some error occurred, although the email may have been sent successfully
1954 if (errmsgs.count() > 1)
1955 qCDebug(KALARM_LOG) << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1];
1956 MessageWin::showError(data.event, data.alarm.dateTime(), errmsgs);
1958 else if (data.queued)
1959 Q_EMIT execAlarmSuccess();
1962 /******************************************************************************
1963 * Execute the command specified in a command alarm.
1964 * To connect to the output ready signals of the process, specify a slot to be
1965 * called by supplying 'receiver' and 'slot' parameters.
1967 ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, const QObject* receiver, const char* slot)
1969 // doShellCommand() will error if the user is not authorised to run
1970 // shell commands.
1971 int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0)
1972 | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0);
1973 QString command = event.cleanText();
1974 if (event.commandScript())
1976 // Store the command script in a temporary file for execution
1977 qCDebug(KALARM_LOG) << "Script";
1978 QString tmpfile = createTempScriptFile(command, false, event, alarm);
1979 if (tmpfile.isEmpty())
1981 setEventCommandError(event, KAEvent::CMD_ERROR);
1982 return Q_NULLPTR;
1984 return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slot);
1986 else
1988 qCDebug(KALARM_LOG) << command;
1989 return doShellCommand(command, event, &alarm, flags, receiver, slot);
1993 /******************************************************************************
1994 * Execute a shell command line specified by an alarm.
1995 * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via
1996 * execAlarm() once the command completes, the execAlarm() parameters being
1997 * derived from the remaining bits in 'flags'.
1998 * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is
1999 * a pre- or post-alarm action respectively.
2000 * To connect to the output ready signals of the process, specify a slot to be
2001 * called by supplying 'receiver' and 'slot' parameters.
2003 * Note that if shell access is not authorised, the attempt to run the command
2004 * will be errored.
2006 ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, const QObject* receiver, const char* slot)
2008 qCDebug(KALARM_LOG) << command << "," << event.id();
2009 QIODevice::OpenMode mode = QIODevice::WriteOnly;
2010 QString cmd;
2011 QString tmpXtermFile;
2012 if (flags & ProcData::EXEC_IN_XTERM)
2014 // Execute the command in a terminal window.
2015 cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile);
2017 else
2019 cmd = command;
2020 mode = QIODevice::ReadWrite;
2023 ProcData* pd = Q_NULLPTR;
2024 ShellProcess* proc = Q_NULLPTR;
2025 if (!cmd.isEmpty())
2027 // Use ShellProcess, which automatically checks whether the user is
2028 // authorised to run shell commands.
2029 proc = new ShellProcess(cmd);
2030 proc->setEnv(QStringLiteral("KALARM_UID"), event.id(), true);
2031 proc->setOutputChannelMode(KProcess::MergedChannels); // combine stdout & stderr
2032 connect(proc, &ShellProcess::shellExited, this, &KAlarmApp::slotCommandExited);
2033 if ((flags & ProcData::DISP_OUTPUT) && receiver && slot)
2035 connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slot);
2036 connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slot);
2038 if (mode == QIODevice::ReadWrite && !event.logFile().isEmpty())
2040 // Output is to be appended to a log file.
2041 // Set up a logging process to write the command's output to.
2042 QString heading;
2043 if (alarm && alarm->dateTime().isValid())
2045 QString dateTime = alarm->dateTime().formatLocale();
2046 heading.sprintf("\n******* KAlarm %s *******\n", dateTime.toLatin1().data());
2048 else
2049 heading = QStringLiteral("\n******* KAlarm *******\n");
2050 QFile logfile(event.logFile());
2051 if (logfile.open(QIODevice::Append | QIODevice::Text))
2053 QTextStream out(&logfile);
2054 out << heading;
2055 logfile.close();
2057 proc->setStandardOutputFile(event.logFile(), QIODevice::Append);
2059 pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : Q_NULLPTR), flags);
2060 if (flags & ProcData::TEMP_FILE)
2061 pd->tempFiles += command;
2062 if (!tmpXtermFile.isEmpty())
2063 pd->tempFiles += tmpXtermFile;
2064 mCommandProcesses.append(pd);
2065 if (proc->start(mode))
2066 return proc;
2069 // Error executing command - report it
2070 qCWarning(KALARM_LOG) << "Command failed to start";
2071 commandErrorMsg(proc, event, alarm, flags);
2072 if (pd)
2074 mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd));
2075 delete pd;
2077 return Q_NULLPTR;
2080 /******************************************************************************
2081 * Compose a command line to execute the given command in a terminal window.
2082 * 'tempScriptFile' receives the name of a temporary script file which is
2083 * invoked by the command line, if applicable.
2084 * Reply = command line, or empty string if error.
2086 QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const
2088 qCDebug(KALARM_LOG) << command << "," << event.id();
2089 tempScriptFile.clear();
2090 QString cmd = Preferences::cmdXTermCommand();
2091 cmd.replace(QLatin1String("%t"), KAboutData::applicationData().displayName()); // set the terminal window title
2092 if (cmd.indexOf(QLatin1String("%C")) >= 0)
2094 // Execute the command from a temporary script file
2095 if (flags & ProcData::TEMP_FILE)
2096 cmd.replace(QLatin1String("%C"), command); // the command is already calling a temporary file
2097 else
2099 tempScriptFile = createTempScriptFile(command, true, event, *alarm);
2100 if (tempScriptFile.isEmpty())
2101 return QString();
2102 cmd.replace(QLatin1String("%C"), tempScriptFile); // %C indicates where to insert the command
2105 else if (cmd.indexOf(QLatin1String("%W")) >= 0)
2107 // Execute the command from a temporary script file,
2108 // with a sleep after the command is executed
2109 tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm);
2110 if (tempScriptFile.isEmpty())
2111 return QString();
2112 cmd.replace(QLatin1String("%W"), tempScriptFile); // %w indicates where to insert the command
2114 else if (cmd.indexOf(QLatin1String("%w")) >= 0)
2116 // Append a sleep to the command.
2117 // Quote the command in case it contains characters such as [>|;].
2118 QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400"));
2119 cmd.replace(QLatin1String("%w"), exec); // %w indicates where to insert the command string
2121 else
2123 // Set the command to execute.
2124 // Put it in quotes in case it contains characters such as [>|;].
2125 QString exec = KShell::quoteArg(command);
2126 if (cmd.indexOf(QLatin1String("%c")) >= 0)
2127 cmd.replace(QLatin1String("%c"), exec); // %c indicates where to insert the command string
2128 else
2129 cmd.append(exec); // otherwise, simply append the command string
2131 return cmd;
2134 /******************************************************************************
2135 * Create a temporary script file containing the specified command string.
2136 * Reply = path of temporary file, or null string if error.
2138 QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const
2140 QTemporaryFile tmpFile;
2141 tmpFile.setAutoRemove(false); // don't delete file when it is destructed
2142 if (!tmpFile.open())
2143 qCCritical(KALARM_LOG) << "Unable to create a temporary script file";
2144 else
2146 tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser);
2147 QTextStream stream(&tmpFile);
2148 if (insertShell)
2149 stream << "#!" << ShellProcess::shellPath() << "\n";
2150 stream << command;
2151 stream.flush();
2152 if (tmpFile.error() != QFile::NoError)
2153 qCCritical(KALARM_LOG) << "Error" << tmpFile.errorString() << " writing to temporary script file";
2154 else
2155 return tmpFile.fileName();
2158 QStringList errmsgs(i18nc("@info", "Error creating temporary script file"));
2159 MessageWin::showError(event, alarm.dateTime(), errmsgs, QStringLiteral("Script"));
2160 return QString();
2163 /******************************************************************************
2164 * Called when a command alarm's execution completes.
2166 void KAlarmApp::slotCommandExited(ShellProcess* proc)
2168 qCDebug(KALARM_LOG);
2169 // Find this command in the command list
2170 for (int i = 0, end = mCommandProcesses.count(); i < end; ++i)
2172 ProcData* pd = mCommandProcesses[i];
2173 if (pd->process == proc)
2175 // Found the command. Check its exit status.
2176 bool executeAlarm = pd->preAction();
2177 ShellProcess::Status status = proc->status();
2178 if (status == ShellProcess::SUCCESS && !proc->exitCode())
2180 qCDebug(KALARM_LOG) << pd->event->id() << ": SUCCESS";
2181 clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE
2182 : pd->postAction() ? KAEvent::CMD_ERROR_POST
2183 : KAEvent::CMD_ERROR);
2185 else
2187 QString errmsg = proc->errorMessage();
2188 if (status == ShellProcess::SUCCESS || status == ShellProcess::NOT_FOUND)
2189 qCWarning(KALARM_LOG) << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode();
2190 else
2191 qCWarning(KALARM_LOG) << pd->event->id() << ":" << errmsg << "exit status =" << status;
2192 if (pd->messageBoxParent)
2194 // Close the existing informational KMessageBox for this process
2195 QList<QDialog*> dialogs = pd->messageBoxParent->findChildren<QDialog*>();
2196 if (!dialogs.isEmpty())
2197 delete dialogs[0];
2198 setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE
2199 : pd->postAction() ? KAEvent::CMD_ERROR_POST
2200 : KAEvent::CMD_ERROR);
2201 if (!pd->tempFile())
2203 errmsg += QLatin1Char('\n');
2204 errmsg += proc->command();
2206 KAMessageBox::error(pd->messageBoxParent, errmsg);
2208 else
2209 commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags);
2211 if (executeAlarm
2212 && (pd->event->extraActionOptions() & KAEvent::CancelOnPreActError))
2214 qCDebug(KALARM_LOG) << pd->event->id() << ": pre-action failed: cancelled";
2215 if (pd->reschedule())
2216 rescheduleAlarm(*pd->event, *pd->alarm, true);
2217 executeAlarm = false;
2220 if (pd->preAction())
2221 AlarmCalendar::resources()->setAlarmPending(pd->event, false);
2222 if (executeAlarm)
2223 execAlarm(*pd->event, *pd->alarm, pd->reschedule(), pd->allowDefer(), true);
2224 mCommandProcesses.removeAt(i);
2225 delete pd;
2226 break;
2230 // If there are now no executing shell commands, quit if a quit was queued
2231 if (mPendingQuit && mCommandProcesses.isEmpty())
2232 quitIf(mPendingQuitCode);
2235 /******************************************************************************
2236 * Output an error message for a shell command, and record the alarm's error status.
2238 void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags)
2240 KAEvent::CmdErrType cmderr;
2241 QStringList errmsgs;
2242 QString dontShowAgain;
2243 if (flags & ProcData::PRE_ACTION)
2245 if (event.extraActionOptions() & KAEvent::DontShowPreActError)
2246 return; // don't notify user of any errors for the alarm
2247 errmsgs += i18nc("@info", "Pre-alarm action:");
2248 dontShowAgain = QStringLiteral("Pre");
2249 cmderr = KAEvent::CMD_ERROR_PRE;
2251 else if (flags & ProcData::POST_ACTION)
2253 errmsgs += i18nc("@info", "Post-alarm action:");
2254 dontShowAgain = QStringLiteral("Post");
2255 cmderr = (event.commandError() == KAEvent::CMD_ERROR_PRE)
2256 ? KAEvent::CMD_ERROR_PRE_POST : KAEvent::CMD_ERROR_POST;
2258 else
2260 dontShowAgain = QStringLiteral("Exec");
2261 cmderr = KAEvent::CMD_ERROR;
2264 // Record the alarm's error status
2265 setEventCommandError(event, cmderr);
2266 // Display an error message
2267 if (proc)
2269 errmsgs += proc->errorMessage();
2270 if (!(flags & ProcData::TEMP_FILE))
2271 errmsgs += proc->command();
2272 dontShowAgain += QString::number(proc->status());
2274 MessageWin::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain);
2277 /******************************************************************************
2278 * Notes that an informational KMessageBox is displayed for this process.
2280 void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent)
2282 // Find this command in the command list
2283 for (int i = 0, end = mCommandProcesses.count(); i < end; ++i)
2285 ProcData* pd = mCommandProcesses[i];
2286 if (pd->process == proc)
2288 pd->messageBoxParent = parent;
2289 break;
2294 /******************************************************************************
2295 * If this is the first time through, open the calendar file, and start
2296 * processing the execution queue.
2298 bool KAlarmApp::initCheck(bool calendarOnly, bool waitForCollection, Akonadi::Collection::Id collectionId)
2300 static bool firstTime = true;
2301 if (firstTime)
2302 qCDebug(KALARM_LOG) << "first time";
2304 if (initialise() || firstTime)
2306 /* Need to open the display calendar now, since otherwise if display
2307 * alarms are immediately due, they will often be processed while
2308 * MessageWin::redisplayAlarms() is executing open() (but before open()
2309 * completes), which causes problems!!
2311 AlarmCalendar::displayCalendar()->open();
2313 if (!AlarmCalendar::resources()->open())
2314 return false;
2316 if (firstTime)
2318 setArchivePurgeDays();
2320 // Warn the user if there are no writable active alarm calendars
2321 checkWritableCalendar();
2323 firstTime = false;
2326 if (!calendarOnly)
2327 startProcessQueue(); // start processing the execution queue
2329 if (waitForCollection)
2331 // Wait for one or all Akonadi collections to be populated
2332 if (!CollectionControlModel::instance()->waitUntilPopulated(collectionId, AKONADI_TIMEOUT))
2333 return false;
2335 return true;
2338 /******************************************************************************
2339 * Called when an audio thread starts or stops.
2341 void KAlarmApp::notifyAudioPlaying(bool playing)
2343 Q_EMIT audioPlaying(playing);
2346 /******************************************************************************
2347 * Stop audio play.
2349 void KAlarmApp::stopAudio()
2351 MessageWin::stopAudio();
2355 void setEventCommandError(const KAEvent& event, KAEvent::CmdErrType err)
2357 if (err == KAEvent::CMD_ERROR_POST && event.commandError() == KAEvent::CMD_ERROR_PRE)
2358 err = KAEvent::CMD_ERROR_PRE_POST;
2359 event.setCommandError(err);
2360 KAEvent* ev = AlarmCalendar::resources()->event(EventId(event));
2361 if (ev && ev->commandError() != err)
2362 ev->setCommandError(err);
2363 AkonadiModel::instance()->updateCommandError(event);
2366 void clearEventCommandError(const KAEvent& event, KAEvent::CmdErrType err)
2368 KAEvent::CmdErrType newerr = static_cast<KAEvent::CmdErrType>(event.commandError() & ~err);
2369 event.setCommandError(newerr);
2370 KAEvent* ev = AlarmCalendar::resources()->event(EventId(event));
2371 if (ev)
2373 newerr = static_cast<KAEvent::CmdErrType>(ev->commandError() & ~err);
2374 ev->setCommandError(newerr);
2376 AkonadiModel::instance()->updateCommandError(event);
2380 KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f)
2381 : process(p),
2382 event(e),
2383 alarm(a),
2384 messageBoxParent(Q_NULLPTR),
2385 flags(f)
2388 KAlarmApp::ProcData::~ProcData()
2390 while (!tempFiles.isEmpty())
2392 // Delete the temporary file called by the XTerm command
2393 QFile f(tempFiles.first());
2394 f.remove();
2395 tempFiles.removeFirst();
2397 delete process;
2398 delete event;
2399 delete alarm;
2402 // vim: et sw=4: