/* * alarmdaemon.cpp - alarm daemon control routines * Program: KAlarm's alarm daemon (kalarmd) * Copyright © 2001,2004-2007 by David Jarvie * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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 TQString START_OF_DAY(TQString::fromLatin1("StartOfDay")); static const TQString AUTOSTART_TRAY(TQString::fromLatin1("AutostartTray")); AlarmDaemon::AlarmDaemon(bool autostart, TQObject *parent, const char *name) : DCOPObject(name), TQObject(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. */ TDEConfig kaconfig(locate("config", "kalarmrc")); kaconfig.setGroup(TQString::fromLatin1("General")); autostart = kaconfig.readBoolEntry(AUTOSTART_TRAY, false); if (autostart) { kdDebug(5900) << "AlarmDaemon::AlarmDaemon(): wait to autostart KAlarm\n"; TQTimer::singleShot(KALARM_AUTOSTART_TIMEOUT * 1000, this, TQT_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"; TQStringList args; args << TQString::fromLatin1("--tray"); int ret = TDEApplication::tdeinitExec(TQString::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 TQTimer(this); connect(mAlarmTimer, TQT_SIGNAL(timeout()), TQT_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 TQString& 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 TQCString& appname, const TQString& 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, TQT_SIGNAL(loaded(ADCalendar*, bool)), TQT_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 TQCString& appname, const TQString& calendarUrl, const TQString& eventID, bool reload) { TQString 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 TQCString& appName, const TQString& appTitle, const TQCString& dcopObject, const TQString& 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 TQCString& 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 - TQTime::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; TQDateTime now = TQDateTime::currentDateTime(); kdDebug(5901) << " To: " << now.toString() << endl; TQValueList alarms = cal->alarmsTo(now); if (!alarms.count()) return; TQValueList eventsDone; for (TQValueList::ConstIterator it = alarms.begin(); it != alarms.end(); ++it) { KCal::Event* event = dynamic_cast((*it)->parent()); if (!event || eventsDone.find(event) != eventsDone.end()) continue; // either not an event, or the event has already been processed eventsDone += event; const TQString& 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 TQStringList cats = event->categories(); bool floats = (cats.find(TQString::fromLatin1("DATE")) != cats.end()); TQDateTime nextDateTime = event->dtStart(); if (recurs) { TQString prop = event->customProperty("KALARM", "NEXTRECUR"); if (prop.length() >= 8) { // The next due recurrence time is specified TQDate 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] == TQChar('T')) { TQTime t(prop.mid(9,2).toInt(), prop.mid(11,2).toInt(), prop.mid(13,2).toInt()); if (t.isValid()) nextDateTime = TQDateTime(d, t); } } } } if (floats) nextDateTime.setTime(mStartOfDay); TQValueList alarmtimes; KCal::Alarm::List alarms = event->alarms(); for (KCal::Alarm::List::ConstIterator al = alarms.begin(); al != alarms.end(); ++al) { KCal::Alarm* alarm = *al; TQDateTime dt; if (alarm->enabled()) { TQDateTime 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 = TQDateTime(); } } // 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; TQDateTime 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 TQString& eventID) { if (!calendar) return true; TQCString 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; TQString id = TQString::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(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 TDEProcess p; TQString 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(TDEProcess::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 - TQTime::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; TQCString appname = client->appName(); if (kapp->dcopClient()->isApplicationRegistered(static_cast(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() { TDEConfig config(locate("config", "kalarmrc")); config.setGroup(TQString::fromLatin1("General")); TQDateTime defTime(TQDate(1900,1,1), TQTime()); 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.) */ TQString AlarmDaemon::expandURL(const TQString& urlString) { if (urlString.isEmpty()) return TQString(); return KURL(urlString).url(); }