/*
* Evolution calendar - Low-level alarm timer mechanism
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) version 3.
*
* 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with the program; if not, see <http://www.gnu.org/licenses/>
*
*
* Authors:
* Miguel de Icaza <miguel@ximian.com>
* Federico Mena-Quintero <federico@ximian.com>
*
* Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
*
*/
#include <config.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <gdk/gdk.h>
#include "alarm.h"
/* Our glib timeout */
static guint timeout_id;
/* The list of pending alarms */
static GList *alarms = NULL;
/* A queued alarm structure */
typedef struct {
time_t trigger;
AlarmFunction alarm_fn;
gpointer data;
AlarmDestroyNotify destroy_notify_fn;
} AlarmRecord;
static void setup_timeout (void);
/* Removes the head alarm from the queue. Does not touch the timeout_id. */
static void
pop_alarm (void)
{
AlarmRecord *ar;
GList *l;
if (!alarms) {
g_warning ("Nothing to pop from the alarm queue");
return;
}
ar = alarms->data;
l = alarms;
alarms = g_list_delete_link (alarms, l);
g_free (ar);
}
/* Callback from the alarm timeout */
static gboolean
alarm_ready_cb (gpointer data)
{
time_t now;
if (!alarms) {
g_warning ("Alarm triggered, but no alarm present\n");
return FALSE;
}
timeout_id = 0;
now = time (NULL);
g_message ("Alarm callback!");
while (alarms) {
AlarmRecord *notify_id, *ar;
AlarmRecord ar_copy;
ar = alarms->data;
if (ar->trigger > now)
break;
g_message ("Process alarm with trigger %lu", ar->trigger);
notify_id = ar;
ar_copy = *ar;
ar = &ar_copy;
pop_alarm (); /* This will free the original AlarmRecord; that's why we copy it */
(* ar->alarm_fn) (notify_id, ar->trigger, ar->data);
if (ar->destroy_notify_fn)
(* ar->destroy_notify_fn) (notify_id, ar->data);
}
/* We need this check because one of the alarm_fn above may have
* re-entered and added an alarm of its own, so the timer will
* already be set up.
*/
if (alarms)
setup_timeout ();
return FALSE;
}
/* Sets up a timeout for the next minute. We do not need to be concerned with
* timezones here, as this is just a periodic check on the alarm queue.
*/
static void
setup_timeout (void)
{
const AlarmRecord *ar;
guint diff;
time_t now;
if (!alarms) {
g_warning ("No alarm to setup\n");
return;
}
ar = alarms->data;
/* Remove the existing time out */
if (timeout_id != 0) {
g_source_remove (timeout_id);
timeout_id = 0;
}
/* Ensure that if the trigger managed to get behind the
* current time we timeout immediately */
diff = MAX (0, ar->trigger - time (NULL));
now = time (NULL);
/* Add the time out */
g_message ("Setting timeout for %d %lu %lu", diff, ar->trigger, now);
g_message (" %s", ctime (&ar->trigger));
g_message (" %s", ctime (&now));
timeout_id = g_timeout_add_seconds (diff, alarm_ready_cb, NULL);
}
/* Used from g_list_insert_sorted(); compares the
* trigger times of two AlarmRecord structures. */
static gint
compare_alarm_by_time (gconstpointer a, gconstpointer b)
{
const AlarmRecord *ara = a;
const AlarmRecord *arb = b;
time_t diff;
diff = ara->trigger - arb->trigger;
return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}
/* Adds an alarm to the queue and sets up the timer */
static void
queue_alarm (AlarmRecord *ar)
{
GList *old_head;
/* Track the current head of the list in case there are changes */
old_head = alarms;
/* Insert the new alarm in order if the alarm's trigger time is
after the current time */
alarms = g_list_insert_sorted (alarms, ar, compare_alarm_by_time);
/* If there first item on the list didn't change, the time out is fine */
if (old_head == alarms)
return;
/* Set the timer for removal upon activation */
setup_timeout ();
}
/**
* alarm_add:
* @trigger: Time at which alarm will trigger.
* @alarm_fn: Callback for trigger.
* @data: Closure data for callback.
* @destroy_notify_fn: destroy notification callback.
*
* Adds an alarm to trigger at the specified time. The @alarm_fn will be called
* with the provided data and the alarm will be removed from the trigger list.
*
* Return value: An identifier for this alarm; it can be used to remove the
* alarm later with alarm_remove(). If the trigger time occurs in the past, then
* the alarm will not be queued and the function will return NULL.
**/
gpointer
alarm_add (time_t trigger, AlarmFunction alarm_fn, gpointer data,
AlarmDestroyNotify destroy_notify_fn)
{
AlarmRecord *ar;
g_return_val_if_fail (trigger != -1, NULL);
g_return_val_if_fail (alarm_fn != NULL, NULL);
ar = g_new (AlarmRecord, 1);
ar->trigger = trigger;
ar->alarm_fn = alarm_fn;
ar->data = data;
ar->destroy_notify_fn = destroy_notify_fn;
queue_alarm (ar);
return ar;
}
/**
* alarm_remove:
* @alarm: A queued alarm identifier.
*
* Removes an alarm from the alarm queue.
**/
void
alarm_remove (gpointer alarm)
{
AlarmRecord *notify_id, *ar;
AlarmRecord ar_copy;
AlarmRecord *old_head;
GList *l;
g_return_if_fail (alarm != NULL);
ar = alarm;
l = g_list_find (alarms, ar);
if (!l) {
g_message (G_STRLOC ": Requested removal of nonexistent alarm!");
return;
}
old_head = alarms->data;
notify_id = ar;
if (old_head == ar) {
ar_copy = *ar;
ar = &ar_copy;
pop_alarm (); /* This will free the original AlarmRecord; that's why we copy it */
} else {
alarms = g_list_delete_link (alarms, l);
}
/* Reset the timeout */
if (!alarms) {
g_source_remove (timeout_id);
timeout_id = 0;
}
/* Notify about destructiono of the alarm */
if (ar->destroy_notify_fn)
(* ar->destroy_notify_fn) (notify_id, ar->data);
}
/**
* alarm_done:
*
* Terminates the alarm timer mechanism. This should be called at the end of
* the program.
**/
void
alarm_done (void)
{
GList *l;
if (timeout_id == 0) {
if (alarms)
g_warning ("No timeout, but queue is not NULL\n");
return;
}
g_source_remove (timeout_id);
timeout_id = 0;
if (!alarms) {
g_warning ("timeout present, freed, but no alarms active\n");
return;
}
for (l = alarms; l; l = l->next) {
AlarmRecord *ar;
ar = l->data;
if (ar->destroy_notify_fn)
(* ar->destroy_notify_fn) (ar, ar->data);
g_free (ar);
}
g_list_free (alarms);
alarms = NULL;
}