diff options
Diffstat (limited to 'kalarm/kalarmd/alarmdaemon.cpp')
-rw-r--r-- | kalarm/kalarmd/alarmdaemon.cpp | 614 |
1 files changed, 614 insertions, 0 deletions
diff --git a/kalarm/kalarmd/alarmdaemon.cpp b/kalarm/kalarmd/alarmdaemon.cpp new file mode 100644 index 000000000..576678574 --- /dev/null +++ b/kalarm/kalarmd/alarmdaemon.cpp @@ -0,0 +1,614 @@ +/* + * alarmdaemon.cpp - alarm daemon control routines + * Program: KAlarm's alarm daemon (kalarmd) + * Copyright © 2001,2004-2007 by David Jarvie <software@astrojar.org.uk> + * Based on the original, (c) 1998, 1999 Preston Brown + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kalarmd.h" + +#include <unistd.h> +#include <stdlib.h> + +#include <qtimer.h> +#include <qfile.h> +#include <qdatetime.h> + +#include <kapplication.h> +#include <kstandarddirs.h> +#include <kprocess.h> +#include <kio/netaccess.h> +#include <dcopclient.h> +#include <kconfig.h> +#include <kdebug.h> + +#include <libkcal/calendarlocal.h> +#include <libkcal/icalformat.h> + +#include "adcalendar.h" +#include "adconfigdata.h" +#include "alarmguiiface.h" +#include "alarmguiiface_stub.h" +#include "alarmdaemon.moc" + + +#ifdef AUTOSTART_KALARM +// Number of seconds to wait before autostarting KAlarm. +// Allow plenty of time for session restoration to happen first. +static const int KALARM_AUTOSTART_TIMEOUT = 30; +#endif +static const int SECS_PER_DAY = 3600 * 24; + +// KAlarm config file keys +static const QString START_OF_DAY(QString::fromLatin1("StartOfDay")); +static const QString AUTOSTART_TRAY(QString::fromLatin1("AutostartTray")); + + +AlarmDaemon::AlarmDaemon(bool autostart, QObject *parent, const char *name) + : DCOPObject(name), + QObject(parent, name), + mAlarmTimer(0) +{ + kdDebug(5900) << "AlarmDaemon::AlarmDaemon()" << endl; + ADConfigData::readConfig(); + + ADConfigData::enableAutoStart(true); // switch autostart on whenever the program is run + + readKAlarmConfig(); // read time-related KAlarm config items + +#ifdef AUTOSTART_KALARM + if (autostart) + { + /* The alarm daemon is being autostarted. + * Check if KAlarm needs to be autostarted in the system tray. + * This should ideally be handled internally by KAlarm, but is done by kalarmd + * for the following reason: + * KAlarm needs to be both session restored and autostarted, but KDE doesn't + * currently cater properly for this - there is no guarantee that the session + * restoration activation will come before the autostart activation. If they + * come in the wrong order, KAlarm won't know that it is supposed to restore + * itself and instead will simply open a new window. + */ + KConfig kaconfig(locate("config", "kalarmrc")); + kaconfig.setGroup(QString::fromLatin1("General")); + autostart = kaconfig.readBoolEntry(AUTOSTART_TRAY, false); + if (autostart) + { + kdDebug(5900) << "AlarmDaemon::AlarmDaemon(): wait to autostart KAlarm\n"; + QTimer::singleShot(KALARM_AUTOSTART_TIMEOUT * 1000, this, SLOT(autostartKAlarm())); + } + } + if (!autostart) +#endif + startMonitoring(); // otherwise, start monitoring alarms now +} + +/****************************************************************************** +* DCOP call to quit the program. +*/ +void AlarmDaemon::quit() +{ + kdDebug(5900) << "AlarmDaemon::quit()" << endl; + exit(0); +} + +/****************************************************************************** +* Called after a timer delay to autostart KAlarm in the system tray. +*/ +void AlarmDaemon::autostartKAlarm() +{ +#ifdef AUTOSTART_KALARM + if (mAlarmTimer) + { + kdDebug(5900) << "AlarmDaemon::autostartKAlarm(): KAlarm already registered\n"; + return; // KAlarm has already registered with us + } + kdDebug(5900) << "AlarmDaemon::autostartKAlarm(): starting KAlarm\n"; + QStringList args; + args << QString::fromLatin1("--tray"); + int ret = KApplication::kdeinitExec(QString::fromLatin1("kalarm"), args); + if (ret) + kdError(5900) << "AlarmDaemon::autostartKAlarm(): error=" << ret << endl; + else + kdDebug(5900) << "AlarmDaemon::autostartKAlarm(): success" << endl; + + startMonitoring(); +#endif +} + +/****************************************************************************** +* Start monitoring alarms. +*/ +void AlarmDaemon::startMonitoring() +{ + // Set up the alarm timer + mAlarmTimer = new QTimer(this); + connect(mAlarmTimer, SIGNAL(timeout()), SLOT(checkAlarmsSlot())); + setTimerStatus(); + + // Start monitoring calendar files. + // They are monitored until their client application registers, upon which + // monitoring ceases until KAlarm tells the daemon to monitor it. + checkAlarms(); +} + +/****************************************************************************** +* DCOP call to enable or disable monitoring of a calendar. +*/ +void AlarmDaemon::enableCal(const QString& urlString, bool enable) +{ + kdDebug(5900) << "AlarmDaemon::enableCal(" << urlString << ")" << endl; + ADCalendar* cal = ADCalendar::getCalendar(urlString); + if (cal) + { + cal->setEnabled(enable); + notifyCalStatus(cal); // notify KAlarm + } +} + +/****************************************************************************** +* DCOP call to reload, and optionally reset, the specified calendar. +*/ +void AlarmDaemon::reloadCal(const QCString& appname, const QString& urlString, bool reset) +{ + kdDebug(5900) << "AlarmDaemon::reloadCal(" << urlString << ")" << endl; + ADCalendar* cal = ADCalendar::getCalendar(urlString); + if (!cal || cal->appName() != appname) + return; + reloadCal(cal, reset); +} + +/****************************************************************************** +* Reload the specified calendar. +* If 'reset' is true, the data associated with the calendar is reset. +*/ +void AlarmDaemon::reloadCal(ADCalendar* cal, bool reset) +{ + kdDebug(5900) << "AlarmDaemon::reloadCal(): calendar" << endl; + if (!cal) + return; + if (!cal->downloading()) + { + cal->close(); + if (!cal->setLoadedConnected()) + connect(cal, SIGNAL(loaded(ADCalendar*, bool)), SLOT(calendarLoaded(ADCalendar*, bool))); + cal->loadFile(reset); + } + else if (reset) + cal->clearEventsHandled(); +} + +void AlarmDaemon::calendarLoaded(ADCalendar* cal, bool success) +{ + if (success) + kdDebug(5900) << "Calendar reloaded" << endl; + notifyCalStatus(cal); // notify KAlarm + setTimerStatus(); + checkAlarms(cal); +} + +/****************************************************************************** +* DCOP call to notify the daemon that an event has been handled, and optionally +* to tell it to reload the calendar. +*/ +void AlarmDaemon::eventHandled(const QCString& appname, const QString& calendarUrl, const QString& eventID, bool reload) +{ + QString urlString = expandURL(calendarUrl); + kdDebug(5900) << "AlarmDaemon::eventHandled(" << urlString << (reload ? "): reload" : ")") << endl; + ADCalendar* cal = ADCalendar::getCalendar(urlString); + if (!cal || cal->appName() != appname) + return; + cal->setEventHandled(eventID); + if (reload) + reloadCal(cal, false); +} + +/****************************************************************************** +* DCOP call to add an application to the list of client applications, +* and add it to the config file. +* N.B. This method must not return a bool because DCOPClient::call() can cause +* a hang if the daemon happens to send a notification to KAlarm at the +* same time as KAlarm calls this DCCOP method. +*/ +void AlarmDaemon::registerApp(const QCString& appName, const QString& appTitle, + const QCString& dcopObject, const QString& calendarUrl, + bool startClient) +{ + kdDebug(5900) << "AlarmDaemon::registerApp(" << appName << ", " << appTitle << ", " + << dcopObject << ", " << startClient << ")" << endl; + KAlarmd::RegisterResult result; + if (appName.isEmpty()) + result = KAlarmd::FAILURE; + else if (startClient && KStandardDirs::findExe(appName).isNull()) + { + kdError() << "AlarmDaemon::registerApp(): app not found" << endl; + result = KAlarmd::NOT_FOUND; + } + else + { + ADCalendar* keepCal = 0; + ClientInfo* client = ClientInfo::get(appName); + if (client) + { + // The application is already a client. + // If it's the same calendar file, don't delete its calendar object. + if (client->calendar() && client->calendar()->urlString() == calendarUrl) + { + keepCal = client->calendar(); + client->detachCalendar(); + } + ClientInfo::remove(appName); // this deletes the calendar if not detached + } + + if (keepCal) + client = new ClientInfo(appName, appTitle, dcopObject, keepCal, startClient); + else + client = new ClientInfo(appName, appTitle, dcopObject, calendarUrl, startClient); + client->calendar()->setUnregistered(false); + ADConfigData::writeClient(appName, client); + + ADConfigData::enableAutoStart(true); + setTimerStatus(); + notifyCalStatus(client->calendar()); + result = KAlarmd::SUCCESS; + } + + // Notify the client of whether the call succeeded. + AlarmGuiIface_stub stub(appName, dcopObject); + stub.registered(false, result, DAEMON_VERSION_NUM); + kdDebug(5900) << "AlarmDaemon::registerApp() -> " << result << endl; +} + +/****************************************************************************** +* DCOP call to change whether KAlarm should be started when an event needs to +* be notified to it. +* N.B. This method must not return a bool because DCOPClient::call() can cause +* a hang if the daemon happens to send a notification to KAlarm at the +* same time as KAlarm calls this DCCOP method. +*/ +void AlarmDaemon::registerChange(const QCString& appName, bool startClient) +{ + kdDebug(5900) << "AlarmDaemon::registerChange(" << appName << ", " << startClient << ")" << endl; + KAlarmd::RegisterResult result; + ClientInfo* client = ClientInfo::get(appName); + if (!client) + return; // can't access client to tell it the result + if (startClient && KStandardDirs::findExe(appName).isNull()) + { + kdError() << "AlarmDaemon::registerChange(): app not found" << endl; + result = KAlarmd::NOT_FOUND; + } + else + { + client->setStartClient(startClient); + ADConfigData::writeClient(appName, client); + result = KAlarmd::SUCCESS; + } + + // Notify the client of whether the call succeeded. + AlarmGuiIface_stub stub(appName, client->dcopObject()); + stub.registered(true, result, DAEMON_VERSION_NUM); + kdDebug(5900) << "AlarmDaemon::registerChange() -> " << result << endl; +} + +/****************************************************************************** +* DCOP call to set autostart at login on or off. +*/ +void AlarmDaemon::enableAutoStart(bool on) +{ + ADConfigData::enableAutoStart(on); +} + +/****************************************************************************** +* Check if any alarms are pending for any enabled calendar, and display the +* pending alarms. +* Called by the alarm timer. +*/ +void AlarmDaemon::checkAlarmsSlot() +{ + kdDebug(5901) << "AlarmDaemon::checkAlarmsSlot()" << endl; + if (mAlarmTimerSyncing) + { + // We've synched to the minute boundary. Now set timer to the check interval. + mAlarmTimer->changeInterval(DAEMON_CHECK_INTERVAL * 1000); + mAlarmTimerSyncing = false; + mAlarmTimerSyncCount = 10; // resynch every 10 minutes, in case of glitches + } + else if (--mAlarmTimerSyncCount <= 0) + { + int interval = DAEMON_CHECK_INTERVAL + 1 - QTime::currentTime().second(); + if (interval < DAEMON_CHECK_INTERVAL - 1) + { + // Need to re-synch to 1 second past the minute + mAlarmTimer->changeInterval(interval * 1000); + mAlarmTimerSyncing = true; + kdDebug(5900) << "Resynching alarm timer" << endl; + } + else + mAlarmTimerSyncCount = 10; + } + checkAlarms(); +} + +/****************************************************************************** +* Check if any alarms are pending for any enabled calendar, and display the +* pending alarms. +*/ +void AlarmDaemon::checkAlarms() +{ + kdDebug(5901) << "AlarmDaemon::checkAlarms()" << endl; + for (ADCalendar::ConstIterator it = ADCalendar::begin(); it != ADCalendar::end(); ++it) + checkAlarms(*it); +} + +/****************************************************************************** +* Check if any alarms are pending for a specified calendar, and display the +* pending alarms. +*/ +void AlarmDaemon::checkAlarms(ADCalendar* cal) +{ + kdDebug(5901) << "AlarmDaemons::checkAlarms(" << cal->urlString() << ")" << endl; + if (!cal->loaded() || !cal->enabled()) + return; + + QDateTime now = QDateTime::currentDateTime(); + kdDebug(5901) << " To: " << now.toString() << endl; + QValueList<KCal::Alarm*> alarms = cal->alarmsTo(now); + if (!alarms.count()) + return; + QValueList<KCal::Event*> eventsDone; + for (QValueList<KCal::Alarm*>::ConstIterator it = alarms.begin(); it != alarms.end(); ++it) + { + KCal::Event* event = dynamic_cast<KCal::Event*>((*it)->parent()); + if (!event || eventsDone.find(event) != eventsDone.end()) + continue; // either not an event, or the event has already been processed + eventsDone += event; + const QString& eventID = event->uid(); + kdDebug(5901) << "AlarmDaemon::checkAlarms(): event " << eventID << endl; + + // Check which of the alarms for this event are due. + // The times in 'alarmtimes' corresponding to due alarms are set. + // The times for non-due alarms are set invalid in 'alarmtimes'. + bool recurs = event->doesRecur(); + const QStringList cats = event->categories(); + bool floats = (cats.find(QString::fromLatin1("DATE")) != cats.end()); + QDateTime nextDateTime = event->dtStart(); + if (recurs) + { + QString prop = event->customProperty("KALARM", "NEXTRECUR"); + if (prop.length() >= 8) + { + // The next due recurrence time is specified + QDate d(prop.left(4).toInt(), prop.mid(4,2).toInt(), prop.mid(6,2).toInt()); + if (d.isValid()) + { + if (floats && prop.length() == 8) + nextDateTime = d; + else if (!floats && prop.length() == 15 && prop[8] == QChar('T')) + { + QTime t(prop.mid(9,2).toInt(), prop.mid(11,2).toInt(), prop.mid(13,2).toInt()); + if (t.isValid()) + nextDateTime = QDateTime(d, t); + } + } + } + } + if (floats) + nextDateTime.setTime(mStartOfDay); + QValueList<QDateTime> alarmtimes; + KCal::Alarm::List alarms = event->alarms(); + for (KCal::Alarm::List::ConstIterator al = alarms.begin(); al != alarms.end(); ++al) + { + KCal::Alarm* alarm = *al; + QDateTime dt; + if (alarm->enabled()) + { + QDateTime dt1; + if (!alarm->hasTime()) + { + // Find the latest recurrence for the alarm. + // Need to do this for alarms with offsets in order to detect + // reminders due for recurrences. + int offset = alarm->hasStartOffset() ? alarm->startOffset().asSeconds() + : alarm->endOffset().asSeconds() + event->dtStart().secsTo(event->dtEnd()); + if (offset) + { + dt1 = nextDateTime.addSecs(floats ? (offset/SECS_PER_DAY)*SECS_PER_DAY : offset); + if (dt1 > now) + dt1 = QDateTime(); + } + } + // Get latest due repetition, or the recurrence time if none + dt = nextDateTime; + if (nextDateTime <= now && alarm->repeatCount() > 0) + { + int snoozeSecs = alarm->snoozeTime() * 60; + int offset = alarm->repeatCount() * snoozeSecs; + QDateTime lastRepetition = nextDateTime.addSecs(floats ? (offset/SECS_PER_DAY)*SECS_PER_DAY : offset); + if (lastRepetition <= now) + dt = lastRepetition; + else + { + int repetition = nextDateTime.secsTo(now) / snoozeSecs; + int offset = repetition * snoozeSecs; + dt = nextDateTime.addSecs(floats ? (offset/SECS_PER_DAY)*SECS_PER_DAY : offset); + } + } + if (!dt.isValid() || dt > now + || dt1.isValid() && dt1 > dt) // already tested dt1 <= now + dt = dt1; + } + alarmtimes.append(dt); + } + if (!cal->eventHandled(event, alarmtimes)) + { + if (notifyEvent(cal, eventID)) + cal->setEventPending(event, alarmtimes); + } + } +} + +/****************************************************************************** +* Send a DCOP message to KAlarm telling it that an alarm should now be handled. +* Reply = false if the event should be held pending until KAlarm can be started. +*/ +bool AlarmDaemon::notifyEvent(ADCalendar* calendar, const QString& eventID) +{ + if (!calendar) + return true; + QCString appname = calendar->appName(); + const ClientInfo* client = ClientInfo::get(appname); + if (!client) + { + kdDebug(5900) << "AlarmDaemon::notifyEvent(" << appname << "): unknown client" << endl; + return false; + } + kdDebug(5900) << "AlarmDaemon::notifyEvent(" << appname << ", " << eventID << "): notification type=" << client->startClient() << endl; + QString id = QString::fromLatin1("ad:") + eventID; // prefix to indicate that the notification if from the daemon + + // Check if the client application is running and ready to receive notification + bool registered = kapp->dcopClient()->isApplicationRegistered(static_cast<const char*>(appname)); + bool ready = registered; + if (registered) + { + // It's running, but check if it has created our DCOP interface yet + QCStringList objects = kapp->dcopClient()->remoteObjects(appname); + if (objects.find(client->dcopObject()) == objects.end()) + ready = false; + } + if (!ready) + { + // KAlarm is not running, or is not yet ready to receive notifications. + if (!client->startClient()) + { + if (registered) + kdDebug(5900) << "AlarmDaemon::notifyEvent(): client not ready\n"; + else + kdDebug(5900) << "AlarmDaemon::notifyEvent(): don't start client\n"; + return false; + } + + // Start KAlarm, using the command line to specify the alarm + KProcess p; + QString cmd = locate("exe", appname); + if (cmd.isEmpty()) + { + kdDebug(5900) << "AlarmDaemon::notifyEvent(): '" << appname << "' not found" << endl; + return true; + } + p << cmd; + p << "--handleEvent" << id << "--calendarURL" << calendar->urlString(); + p.start(KProcess::DontCare); + kdDebug(5900) << "AlarmDaemon::notifyEvent(): used command line" << endl; + return true; + } + + // Notify the client by telling it the calendar URL and event ID + AlarmGuiIface_stub stub(appname, client->dcopObject()); + stub.handleEvent(calendar->urlString(), id); + if (!stub.ok()) + { + kdDebug(5900) << "AlarmDaemon::notifyEvent(): dcop send failed" << endl; + return false; + } + return true; +} + +/****************************************************************************** +* Starts or stops the alarm timer as necessary after a calendar is enabled/disabled. +*/ +void AlarmDaemon::setTimerStatus() +{ +#ifdef AUTOSTART_KALARM + if (!mAlarmTimer) + { + // KAlarm is now running, so start monitoring alarms + startMonitoring(); + return; // startMonitoring() calls this method + } + +#endif + // Count the number of currently loaded calendars whose names should be displayed + int nLoaded = 0; + for (ADCalendar::ConstIterator it = ADCalendar::begin(); it != ADCalendar::end(); ++it) + if ((*it)->loaded()) + ++nLoaded; + + // Start or stop the alarm timer if necessary + if (!mAlarmTimer->isActive() && nLoaded) + { + // Timeout every minute. + // But first synchronise to one second after the minute boundary. + int firstInterval = DAEMON_CHECK_INTERVAL + 1 - QTime::currentTime().second(); + mAlarmTimer->start(1000 * firstInterval); + mAlarmTimerSyncing = (firstInterval != DAEMON_CHECK_INTERVAL); + kdDebug(5900) << "Started alarm timer" << endl; + } + else if (mAlarmTimer->isActive() && !nLoaded) + { + mAlarmTimer->stop(); + kdDebug(5900) << "Stopped alarm timer" << endl; + } +} + +/****************************************************************************** +* Send a DCOP message to to the client which owns the specified calendar, +* notifying it of a change in calendar status. +*/ +void AlarmDaemon::notifyCalStatus(const ADCalendar* cal) +{ + ClientInfo* client = ClientInfo::get(cal); + if (!client) + return; + QCString appname = client->appName(); + if (kapp->dcopClient()->isApplicationRegistered(static_cast<const char*>(appname))) + { + KAlarmd::CalendarStatus change = cal->available() ? (cal->enabled() ? KAlarmd::CALENDAR_ENABLED : KAlarmd::CALENDAR_DISABLED) + : KAlarmd::CALENDAR_UNAVAILABLE; + kdDebug(5900) << "AlarmDaemon::notifyCalStatus() sending:" << appname << " -> " << change << endl; + AlarmGuiIface_stub stub(appname, client->dcopObject()); + stub.alarmDaemonUpdate(change, cal->urlString()); + if (!stub.ok()) + kdError(5900) << "AlarmDaemon::notifyCalStatus(): dcop send failed:" << appname << endl; + } +} + +/****************************************************************************** +* Read all relevant items from KAlarm config. +* Executed on DCOP call to notify a time related value change in the KAlarm +* config file. +*/ +void AlarmDaemon::readKAlarmConfig() +{ + KConfig config(locate("config", "kalarmrc")); + config.setGroup(QString::fromLatin1("General")); + QDateTime defTime(QDate(1900,1,1), QTime()); + mStartOfDay = config.readDateTimeEntry(START_OF_DAY, &defTime).time(); + kdDebug(5900) << "AlarmDaemon::readKAlarmConfig()" << endl; +} + +/****************************************************************************** +* Expand a DCOP call parameter URL to a full URL. +* (We must store full URLs in the calendar data since otherwise later calls to +* reload or remove calendars won't necessarily find a match.) +*/ +QString AlarmDaemon::expandURL(const QString& urlString) +{ + if (urlString.isEmpty()) + return QString(); + return KURL(urlString).url(); +} |