/*
* Alarm queueing engine
*
* 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:
* Federico Mena-Quintero <federico@ximian.com>
*
* Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
*
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
#include <gtk/gtk.h>
#include <glib/gi18n.h>
#ifdef HAVE_CANBERRA
#include <canberra-gtk.h>
#endif
#ifdef HAVE_LIBNOTIFY
#include <libnotify/notify.h>
#endif
#include "alarm.h"
#include "alarm-notify-dialog.h"
#include "alarm-queue.h"
#include "alarm-notify.h"
#include "config-data.h"
#include "util.h"
#include "calendar/gui/print.h"
/* The dialog with alarm nofications */
static AlarmNotificationsDialog *alarm_notifications_dialog = NULL;
/* Whether the queueing system has been initialized */
static gboolean alarm_queue_inited = FALSE;
/* Clients we are monitoring for alarms */
static GHashTable *client_alarms_hash = NULL;
/* List of tray icons being displayed */
static GList *tray_icons_list = NULL;
/* Top Tray Image */
static GtkStatusIcon *tray_icon = NULL;
static gint tray_blink_id = -1;
static gint tray_blink_countdown = 0;
static AlarmNotify *an;
/* Structure that stores a client we are monitoring */
typedef struct {
/* Monitored client */
ECalClient *cal_client;
/* The live view to the calendar */
ECalClientView *view;
/* Hash table of component UID -> CompQueuedAlarms. If an element is
* present here, then it means its cqa->queued_alarms contains at least
* one queued alarm. When all the alarms for a component have been
* dequeued, the CompQueuedAlarms structure is removed from the hash
* table. Thus a CQA exists <=> it has queued alarms.
*/
GHashTable *uid_alarms_hash;
} ClientAlarms;
/* Pair of a ECalComponentAlarms and the mapping from queued alarm IDs to the
* actual alarm instance structures.
*/
typedef struct {
/* The parent client alarms structure */
ClientAlarms *parent_client;
/* The component's UID */
ECalComponentId *id;
/* The actual component and its alarm instances */
ECalComponentAlarms *alarms;
/* List of QueuedAlarm structures */
GSList *queued_alarms;
/* Flags */
gboolean expecting_update;
} CompQueuedAlarms;
/* Pair of a queued alarm ID and the alarm trigger instance it refers to */
typedef struct {
/* Alarm ID from alarm.h */
gpointer alarm_id;
/* Instance from our parent CompQueuedAlarms->alarms->alarms list */
ECalComponentAlarmInstance *instance;
/* original trigger of the instance from component */
time_t orig_trigger;
/* Whether this is a snoozed queued alarm or a normal one */
guint snooze : 1;
} QueuedAlarm;
/* Alarm ID for the midnight refresh function */
static gpointer midnight_refresh_id = NULL;
static time_t midnight = 0;
static void remove_client_alarms (ClientAlarms *ca);
static void display_notification (time_t trigger,
CompQueuedAlarms *cqa,
gpointer alarm_id,
gboolean use_description);
static void audio_notification (time_t trigger,
CompQueuedAlarms *cqa,
gpointer alarm_id);
static void mail_notification (time_t trigger,
CompQueuedAlarms *cqa,
gpointer alarm_id);
static void procedure_notification (time_t trigger,
CompQueuedAlarms *cqa,
gpointer alarm_id);
#ifdef HAVE_LIBNOTIFY
static void popup_notification (time_t trigger,
CompQueuedAlarms *cqa,
gpointer alarm_id,
gboolean use_description);
#endif
static void query_objects_modified_cb (ECalClientView *view,
const GSList *objects,
gpointer data);
static void query_objects_removed_cb (ECalClientView *view,
const GSList *uids,
gpointer data);
static void update_cqa (CompQueuedAlarms *cqa,
ECalComponent *comp);
static void update_qa (ECalComponentAlarms *alarms,
QueuedAlarm *qa);
static void tray_list_remove_cqa (CompQueuedAlarms *cqa);
static void on_dialog_objs_removed_cb (ECalClientView *view,
const GSList *uids,
gpointer data);
/* Alarm queue engine */
static void load_alarms_for_today (ClientAlarms *ca);
static void midnight_refresh_cb (gpointer alarm_id,
time_t trigger,
gpointer data);
/* Simple asynchronous message dispatcher */
typedef struct _Message Message;
typedef void (*MessageFunc) (Message *msg);
struct _Message {
MessageFunc func;
};
/*
static void
message_proxy (Message *msg)
{
g_return_if_fail (msg->func != NULL);
*
msg->func (msg);
}
*
static gpointer
create_thread_pool (void)
{
return g_thread_pool_new ((GFunc) message_proxy, NULL, 1, FALSE, NULL);
}*/
static void
message_push (Message *msg)
{
/* This used be pushed through the thread pool. This fix is made to
* work-around the crashers in dbus due to threading. The threading
* is not completely removed as its better to have alarm daemon
* running in a thread rather than blocking main thread. This is
* the reason the creation of thread pool is commented out. */
msg->func (msg);
}
/*
* use a static ring-buffer so we can call this twice
* in a printf without getting nonsense results.
*/
static const gchar *
e_ctime (const time_t *timep)
{
static gchar *buffer[4] = { 0, };
static gint next = 0;
const gchar *ret;
g_free (buffer[next]);
ret = buffer[next++] = g_strdup (ctime (timep));
if (buffer[next - 1] && *buffer[next - 1]) {
gint len = strlen (buffer[next - 1]);
while (len > 0 && (buffer[next - 1][len - 1] == '\n' ||
buffer[next - 1][len - 1] == '\r' ||
g_ascii_isspace (buffer[next - 1][len - 1])))
len--;
buffer[next - 1][len - 1] = 0;
}
if (next >= G_N_ELEMENTS (buffer))
next = 0;
return ret;
}
/* Queues an alarm trigger for midnight so that we can load the next
* day's worth of alarms. */
static void
queue_midnight_refresh (void)
{
icaltimezone *zone;
if (midnight_refresh_id != NULL) {
alarm_remove (midnight_refresh_id);
midnight_refresh_id = NULL;
}
zone = config_data_get_timezone ();
midnight = time_day_end_with_zone (time (NULL), zone);
debug (("Refresh at %s", e_ctime (&midnight)));
midnight_refresh_id = alarm_add (
midnight, midnight_refresh_cb, NULL, NULL);
if (!midnight_refresh_id) {
debug (("Could not setup the midnight refresh alarm"));
/* FIXME: what to do? */
}
}
/* Loads a client's alarms; called from g_hash_table_foreach() */
static void
add_client_alarms_cb (gpointer key,
gpointer value,
gpointer data)
{
ClientAlarms *ca = (ClientAlarms *) value;
debug (("Adding %p", ca));
load_alarms_for_today (ca);
}
struct _midnight_refresh_msg {
Message header;
gboolean remove;
};
/* Loads the alarms for the new day every midnight */
static void
midnight_refresh_async (struct _midnight_refresh_msg *msg)
{
debug (("..."));
/* Re-load the alarms for all clients */
g_hash_table_foreach (client_alarms_hash, add_client_alarms_cb, NULL);
/* Re-schedule the midnight update */
if (msg->remove && midnight_refresh_id != NULL) {
debug (("Reschedule the midnight update"));
alarm_remove (midnight_refresh_id);
midnight_refresh_id = NULL;
}
queue_midnight_refresh ();
g_slice_free (struct _midnight_refresh_msg, msg);
}
static void
midnight_refresh_cb (gpointer alarm_id,
time_t trigger,
gpointer data)
{
struct _midnight_refresh_msg *msg;
msg = g_slice_new0 (struct _midnight_refresh_msg);
msg->header.func = (MessageFunc) midnight_refresh_async;
msg->remove = TRUE;
message_push ((Message *) msg);
}
/* Looks up a client in the client alarms hash table */
static ClientAlarms *
lookup_client (ECalClient *cal_client)
{
return g_hash_table_lookup (client_alarms_hash, cal_client);
}
/* Looks up a queued alarm based on its alarm ID */
static QueuedAlarm *
lookup_queued_alarm (CompQueuedAlarms *cqa,
gpointer alarm_id)
{
GSList *l;
QueuedAlarm *qa;
qa = NULL;
for (l = cqa->queued_alarms; l; l = l->next) {
qa = l->data;
if (qa->alarm_id == alarm_id)
return qa;
}
/* not found, might have been updated/removed */
return NULL;
}
/* Removes an alarm from the list of alarms of a component. If the alarm was
* the last one listed for the component, it removes the component itself.
*/
static gboolean
remove_queued_alarm (CompQueuedAlarms *cqa,
gpointer alarm_id,
gboolean free_object,
gboolean remove_alarm)
{
QueuedAlarm *qa = NULL;
GSList *l;
debug (("..."));
for (l = cqa->queued_alarms; l; l = l->next) {
qa = l->data;
if (qa->alarm_id == alarm_id)
break;
}
if (!l)
return FALSE;
cqa->queued_alarms = g_slist_delete_link (cqa->queued_alarms, l);
if (remove_alarm) {
GError *error = NULL;
ECalComponentId *id;
id = e_cal_component_get_id (cqa->alarms->comp);
if (id) {
cqa->expecting_update = TRUE;
e_cal_client_discard_alarm_sync (
cqa->parent_client->cal_client, id->uid,
id->rid, qa->instance->auid, NULL, &error);
cqa->expecting_update = FALSE;
if (error) {
if (!g_error_matches (error, E_CLIENT_ERROR, E_CLIENT_ERROR_NOT_SUPPORTED))
g_warning (
"%s: Failed to discard alarm: %s",
G_STRFUNC, error->message);
g_error_free (error);
}
e_cal_component_free_id (id);
}
}
g_free (qa);
/* If this was the last queued alarm for this component, remove the
* component itself.
*/
if (cqa->queued_alarms != NULL)
return FALSE;
debug (("Last Component. Removing CQA- Free=%d", free_object));
if (free_object) {
cqa->id = NULL;
cqa->parent_client = NULL;
e_cal_component_alarms_free (cqa->alarms);
g_free (cqa);
} else {
e_cal_component_alarms_free (cqa->alarms);
cqa->alarms = NULL;
}
return TRUE;
}
/**
* has_known_notification:
* Test for notification method and returns if it knows it or not.
* @comp: Component with an alarm.
* @alarm_uid: ID of the alarm in the comp to test.
*
* Returns: %TRUE when we know the notification type, %FALSE otherwise.
*/
static gboolean
has_known_notification (ECalComponent *comp,
const gchar *alarm_uid)
{
ECalComponentAlarm *alarm;
ECalComponentAlarmAction action;
g_return_val_if_fail (comp != NULL, FALSE);
g_return_val_if_fail (alarm_uid != NULL, FALSE);
alarm = e_cal_component_get_alarm (comp, alarm_uid);
if (!alarm)
return FALSE;
e_cal_component_alarm_get_action (alarm, &action);
e_cal_component_alarm_free (alarm);
switch (action) {
case E_CAL_COMPONENT_ALARM_AUDIO:
case E_CAL_COMPONENT_ALARM_DISPLAY:
case E_CAL_COMPONENT_ALARM_EMAIL:
case E_CAL_COMPONENT_ALARM_PROCEDURE:
return TRUE;
default:
break;
}
return FALSE;
}
/* Callback used when an alarm triggers */
static void
alarm_trigger_cb (gpointer alarm_id,
time_t trigger,
gpointer data)
{
CompQueuedAlarms *cqa;
ECalComponent *comp;
QueuedAlarm *qa;
ECalComponentAlarm *alarm;
ECalComponentAlarmAction action;
cqa = data;
comp = cqa->alarms->comp;
config_data_set_last_notification_time (
cqa->parent_client->cal_client, trigger);
debug (("Setting Last notification time to %s", e_ctime (&trigger)));
qa = lookup_queued_alarm (cqa, alarm_id);
if (!qa)
return;
/* Decide what to do based on the alarm action. We use the trigger that
* is passed to us instead of the one from the instance structure
* because this may be a snoozed alarm instead of an original
* occurrence.
*/
alarm = e_cal_component_get_alarm (comp, qa->instance->auid);
if (!alarm)
return;
e_cal_component_alarm_get_action (alarm, &action);
e_cal_component_alarm_free (alarm);
switch (action) {
case E_CAL_COMPONENT_ALARM_AUDIO:
audio_notification (trigger, cqa, alarm_id);
break;
case E_CAL_COMPONENT_ALARM_DISPLAY:
#ifdef HAVE_LIBNOTIFY
popup_notification (trigger, cqa, alarm_id, TRUE);
#endif
display_notification (trigger, cqa, alarm_id, TRUE);
break;
case E_CAL_COMPONENT_ALARM_EMAIL:
mail_notification (trigger, cqa, alarm_id);
break;
case E_CAL_COMPONENT_ALARM_PROCEDURE:
procedure_notification (trigger, cqa, alarm_id);
break;
default:
g_return_if_reached ();
}
debug (("Notification sent: %d", action));
}
/* Adds the alarms in a ECalComponentAlarms structure to the alarms queued for a
* particular client. Also puts the triggers in the alarm timer queue.
*/
static void
add_component_alarms (ClientAlarms *ca,
ECalComponentAlarms *alarms)
{
ECalComponentId *id;
CompQueuedAlarms *cqa;
GSList *l;
/* No alarms? */
if (alarms == NULL || alarms->alarms == NULL) {
debug (("No alarms to add"));
if (alarms)
e_cal_component_alarms_free (alarms);
return;
}
cqa = g_new (CompQueuedAlarms, 1);
cqa->parent_client = ca;
cqa->alarms = alarms;
cqa->expecting_update = FALSE;
cqa->queued_alarms = NULL;
debug (("Creating CQA %p", cqa));
for (l = alarms->alarms; l; l = l->next) {
ECalComponentAlarmInstance *instance;
gpointer alarm_id;
QueuedAlarm *qa;
instance = l->data;
if (!has_known_notification (cqa->alarms->comp, instance->auid))
continue;
alarm_id = alarm_add (
instance->trigger, alarm_trigger_cb, cqa, NULL);
if (!alarm_id)
continue;
qa = g_new (QueuedAlarm, 1);
qa->alarm_id = alarm_id;
qa->instance = instance;
qa->orig_trigger = instance->trigger;
qa->snooze = FALSE;
cqa->queued_alarms = g_slist_prepend (cqa->queued_alarms, qa);
}
id = e_cal_component_get_id (alarms->comp);
/* If we failed to add all the alarms, then we should get rid of the cqa */
if (cqa->queued_alarms == NULL) {
e_cal_component_alarms_free (cqa->alarms);
cqa->alarms = NULL;
debug (("Failed to add all : %p", cqa));
g_free (cqa);
return;
}
cqa->queued_alarms = g_slist_reverse (cqa->queued_alarms);
cqa->id = id;
debug (("Alarm added for %s", id->uid));
g_hash_table_insert (ca->uid_alarms_hash, cqa->id, cqa);
}
/* Loads the alarms of a client for a given range of time */
static void
load_alarms (ClientAlarms *ca,
time_t start,
time_t end)
{
gchar *str_query, *iso_start, *iso_end;
GError *error = NULL;
debug (("..."));
iso_start = isodate_from_time_t (start);
if (!iso_start)
return;
iso_end = isodate_from_time_t (end);
if (!iso_end) {
g_free (iso_start);
return;
}
str_query = g_strdup_printf (
"(has-alarms-in-range? (make-time \"%s\") "
"(make-time \"%s\"))", iso_start, iso_end);
g_free (iso_start);
g_free (iso_end);
/* create the live query */
if (ca->view) {
debug (("Disconnecting old queries"));
g_signal_handlers_disconnect_matched (
ca->view, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, ca);
g_object_unref (ca->view);
ca->view = NULL;
}
e_cal_client_get_view_sync (
ca->cal_client, str_query, &ca->view, NULL, &error);
if (error != NULL) {
g_warning (
"%s: Could not get query for client: %s",
G_STRFUNC, error->message);
g_error_free (error);
} else {
debug (("Setting Call backs"));
g_signal_connect (
ca->view, "objects-added",
G_CALLBACK (query_objects_modified_cb), ca);
g_signal_connect (
ca->view, "objects-modified",
G_CALLBACK (query_objects_modified_cb), ca);
g_signal_connect (
ca->view, "objects-removed",
G_CALLBACK (query_objects_removed_cb), ca);
e_cal_client_view_start (ca->view, &error);
if (error != NULL) {
g_warning (
"%s: Failed to start view: %s",
G_STRFUNC, error->message);
g_error_free (error);
}
}
g_free (str_query);
}
/* Loads today's remaining alarms for a client */
static void
load_alarms_for_today (ClientAlarms *ca)
{
time_t now, from, day_end, day_start;
icaltimezone *zone;
now = time (NULL);
zone = config_data_get_timezone ();
day_start = time_day_begin_with_zone (now, zone);
/* Make sure we don't miss some events from the last notification.
* We add 1 to the saved notification time to make the time ranges
* half-open; we do not want to display the "last" displayed alarm
* twice, once when it occurs and once when the alarm daemon restarts.
*/
from = config_data_get_last_notification_time (ca->cal_client) + 1;
if (from <= 0)
from = MAX (from, day_start);
/* Add one hour after midnight, just to cover the delay in 30 minutes
* midnight checking. */
day_end = time_day_end_with_zone (now, zone) + (60 * 60);
debug (("From %s to %s", e_ctime (&from), e_ctime (&day_end)));
load_alarms (ca, from, day_end);
}
/* Looks up a component's queued alarm structure in a client alarms structure */
static CompQueuedAlarms *
lookup_comp_queued_alarms (ClientAlarms *ca,
const ECalComponentId *id)
{
return g_hash_table_lookup (ca->uid_alarms_hash, id);
}
static void
remove_alarms (CompQueuedAlarms *cqa,
gboolean free_object)
{
GSList *l;
debug (("Removing for %p", cqa));
tray_list_remove_cqa (cqa);
for (l = cqa->queued_alarms; l;) {
QueuedAlarm *qa;
qa = l->data;
/* Get the next element here because the list element will go
* away in remove_queued_alarm(). The qa will be freed there as
* well.
*/
l = l->next;
alarm_remove (qa->alarm_id);
remove_queued_alarm (cqa, qa->alarm_id, free_object, FALSE);
}
}
/* Removes a component an its alarms */
static void
remove_comp (ClientAlarms *ca,
ECalComponentId *id)
{
CompQueuedAlarms *cqa;
debug (("Removing uid %s", id->uid));
if (id->rid && !(*(id->rid))) {
g_free (id->rid);
id->rid = NULL;
}
cqa = lookup_comp_queued_alarms (ca, id);
if (!cqa)
return;
/* If a component is present, then it means we must have alarms queued
* for it.
*/
g_return_if_fail (cqa->queued_alarms != NULL);
debug (("Removing CQA %p", cqa));
remove_alarms (cqa, TRUE);
}
/* Called when a calendar component changes; we must reload its corresponding
* alarms.
*/
struct _query_msg {
Message header;
GSList *objects;
gpointer data;
};
static GSList *
duplicate_ical (const GSList *in_list)
{
const GSList *l;
GSList *out_list = NULL;
for (l = in_list; l; l = l->next) {
out_list = g_slist_prepend (
out_list, icalcomponent_new_clone (l->data));
}
return g_slist_reverse (out_list);
}
static GSList *
duplicate_ecal (const GSList *in_list)
{
const GSList *l;
GSList *out_list = NULL;
for (l = in_list; l; l = l->next) {
ECalComponentId *id, *old;
old = l->data;
id = g_new0 (ECalComponentId, 1);
id->uid = g_strdup (old->uid);
id->rid = g_strdup (old->rid);
out_list = g_slist_prepend (out_list, id);
}
return g_slist_reverse (out_list);
}
static gboolean
get_alarms_for_object (ECalClient *cal_client,
const ECalComponentId *id,
time_t start,
time_t end,
ECalComponentAlarms **alarms)
{
icalcomponent *icalcomp;
ECalComponent *comp;
ECalComponentAlarmAction omit[] = {-1};
g_return_val_if_fail (cal_client != NULL, FALSE);
g_return_val_if_fail (id != NULL, FALSE);
g_return_val_if_fail (alarms != NULL, FALSE);
g_return_val_if_fail (start >= 0 && end >= 0, FALSE);
g_return_val_if_fail (start <= end, FALSE);
if (!e_cal_client_get_object_sync (
cal_client, id->uid, id->rid, &icalcomp, NULL, NULL))
return FALSE;
if (!icalcomp)
return FALSE;
comp = e_cal_component_new ();
if (!e_cal_component_set_icalcomponent (comp, icalcomp)) {
icalcomponent_free (icalcomp);
g_object_unref (comp);
return FALSE;
}
*alarms = e_cal_util_generate_alarms_for_comp (
comp, start, end, omit, e_cal_client_resolve_tzid_cb,
cal_client, e_cal_client_get_default_timezone (cal_client));
g_object_unref (comp);
return TRUE;
}
static void
query_objects_changed_async (struct _query_msg *msg)
{
ClientAlarms *ca;
time_t from, day_end;
ECalComponentAlarms *alarms;
gboolean found;
icaltimezone *zone;
CompQueuedAlarms *cqa;
GSList *l;
GSList *objects;
ca = msg->data;
objects = msg->objects;
from = config_data_get_last_notification_time (ca->cal_client);
if (from == -1)
from = time (NULL);
else
from += 1; /* we add 1 to make sure the alarm is not displayed twice */
zone = config_data_get_timezone ();
day_end = time_day_end_with_zone (time (NULL), zone);
for (l = objects; l != NULL; l = l->next) {
ECalComponentId *id;
GSList *sl;
ECalComponent *comp = e_cal_component_new ();
e_cal_component_set_icalcomponent (comp, l->data);
id = e_cal_component_get_id (comp);
found = get_alarms_for_object (ca->cal_client, id, from, day_end, &alarms);
if (!found) {
debug (("No Alarm found for client %p", ca->cal_client));
tray_list_remove_cqa (lookup_comp_queued_alarms (ca, id));
remove_comp (ca, id);
g_hash_table_remove (ca->uid_alarms_hash, id);
e_cal_component_free_id (id);
g_object_unref (comp);
comp = NULL;
continue;
}
cqa = lookup_comp_queued_alarms (ca, id);
if (!cqa) {
debug (("No currently queued alarms for %s", id->uid));
add_component_alarms (ca, alarms);
g_object_unref (comp);
comp = NULL;
continue;
}
debug (("Alarm Already Exist for %s", id->uid));
/* If the alarms or the alarms list is empty,
* remove it after updating the cqa structure. */
if (alarms == NULL || alarms->alarms == NULL) {
/* Update the cqa and its queued alarms
* for changes in summary and alarm_uid. */
update_cqa (cqa, comp);
if (alarms)
e_cal_component_alarms_free (alarms);
continue;
}
/* if already in the list, just update it */
remove_alarms (cqa, FALSE);
cqa->alarms = alarms;
cqa->queued_alarms = NULL;
/* add the new alarms */
for (sl = cqa->alarms->alarms; sl; sl = sl->next) {
ECalComponentAlarmInstance *instance;
gpointer alarm_id;
QueuedAlarm *qa;
instance = sl->data;
if (!has_known_notification (cqa->alarms->comp, instance->auid))
continue;
alarm_id = alarm_add (instance->trigger, alarm_trigger_cb, cqa, NULL);
if (!alarm_id)
continue;
qa = g_new (QueuedAlarm, 1);
qa->alarm_id = alarm_id;
qa->instance = instance;
qa->snooze = FALSE;
qa->orig_trigger = instance->trigger;
cqa->queued_alarms = g_slist_prepend (cqa->queued_alarms, qa);
debug (("Adding %p to queue", qa));
}
cqa->queued_alarms = g_slist_reverse (cqa->queued_alarms);
g_object_unref (comp);
comp = NULL;
}
g_slist_free (objects);
g_slice_free (struct _query_msg, msg);
}
static void
query_objects_modified_cb (ECalClientView *view,
const GSList *objects,
gpointer data)
{
struct _query_msg *msg;
msg = g_slice_new0 (struct _query_msg);
msg->header.func = (MessageFunc) query_objects_changed_async;
msg->objects = duplicate_ical (objects);
msg->data = data;
message_push ((Message *) msg);
}
/* Called when a calendar component is removed; we must delete its corresponding
* alarms.
*/
static void
query_objects_removed_async (struct _query_msg *msg)
{
ClientAlarms *ca;
GSList *l;
GSList *objects;
ca = msg->data;
objects = msg->objects;
debug (("Removing %d objects", g_slist_length (objects)));
for (l = objects; l != NULL; l = l->next) {
/* If the alarm is already triggered remove it. */
tray_list_remove_cqa (lookup_comp_queued_alarms (ca, l->data));
remove_comp (ca, l->data);
g_hash_table_remove (ca->uid_alarms_hash, l->data);
e_cal_component_free_id (l->data);
}
g_slist_free (objects);
g_slice_free (struct _query_msg, msg);
}
static void
query_objects_removed_cb (ECalClientView *view,
const GSList *uids,
gpointer data)
{
struct _query_msg *msg;
msg = g_slice_new0 (struct _query_msg);
msg->header.func = (MessageFunc) query_objects_removed_async;
msg->objects = duplicate_ecal (uids);
msg->data = data;
message_push ((Message *) msg);
}
/* Notification functions */
/* Creates a snooze alarm based on an existing one. The snooze offset is
* compued with respect to the current time.
*/
static void
create_snooze (CompQueuedAlarms *cqa,
gpointer alarm_id,
gint snooze_mins)
{
QueuedAlarm *orig_qa;
time_t t;
gpointer new_id;
orig_qa = lookup_queued_alarm (cqa, alarm_id);
if (!orig_qa)
return;
t = time (NULL);
t += snooze_mins * 60;
new_id = alarm_add (t, alarm_trigger_cb, cqa, NULL);
if (!new_id) {
debug (("Unable to schedule trigger for %s", e_ctime (&t)));
return;
}
orig_qa->instance->trigger = t;
orig_qa->alarm_id = new_id;
orig_qa->snooze = TRUE;
debug (("Adding a alarm at %s", e_ctime (&t)));
}
/* Launches a component editor for a component */
static void
edit_component (ECalClient *cal_client,
ECalComponent *comp)
{
ESource *source;
gchar *command_line;
const gchar *scheme;
const gchar *comp_uid;
const gchar *source_uid;
GError *error = NULL;
/* XXX Don't we have a function to construct these URIs?
* How are other apps expected to know this stuff? */
source = e_client_get_source (E_CLIENT (cal_client));
source_uid = e_source_get_uid (source);
e_cal_component_get_uid (comp, &comp_uid);
switch (e_cal_client_get_source_type (cal_client)) {
case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
scheme = "calendar:";
break;
case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
scheme = "task:";
break;
case E_CAL_CLIENT_SOURCE_TYPE_MEMOS:
scheme = "memo:";
break;
default:
g_return_if_reached ();
}
command_line = g_strdup_printf (
"%s %s///?source-uid=%s&comp-uid=%s",
PACKAGE, scheme, source_uid, comp_uid);
if (!g_spawn_command_line_async (command_line, &error)) {
g_critical ("%s", error->message);
g_error_free (error);
}
g_free (command_line);
}
static void
print_component (ECalClient *cal_client,
ECalComponent *comp)
{
print_comp (
comp,
cal_client,
config_data_get_timezone (),
config_data_get_24_hour_format (),
GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG);
}
typedef struct {
gchar *summary;
gchar *description;
gchar *location;
gboolean blink_state;
gboolean snooze_set;
gint blink_id;
time_t trigger;
CompQueuedAlarms *cqa;
gpointer alarm_id;
ECalComponent *comp;
ECalClient *cal_client;
ECalClientView *view;
GdkPixbuf *image;
GtkTreeIter iter;
gboolean is_in_tree;
} TrayIconData;
static void
free_tray_icon_data (TrayIconData *tray_data)
{
g_return_if_fail (tray_data != NULL);
if (tray_data->summary) {
g_free (tray_data->summary);
tray_data->summary = NULL;
}
if (tray_data->description) {
g_free (tray_data->description);
tray_data->description = NULL;
}
if (tray_data->location) {
g_free (tray_data->location);
tray_data->location = NULL;
}
g_object_unref (tray_data->cal_client);
tray_data->cal_client = NULL;
g_signal_handlers_disconnect_matched (
tray_data->view, G_SIGNAL_MATCH_FUNC,
0, 0, NULL, on_dialog_objs_removed_cb, NULL);
g_object_unref (tray_data->view);
tray_data->view = NULL;
g_object_unref (tray_data->comp);
tray_data->comp = NULL;
tray_data->cqa = NULL;
tray_data->alarm_id = NULL;
tray_data->image = NULL;
g_free (tray_data);
}
static void
on_dialog_objs_removed_async (struct _query_msg *msg)
{
TrayIconData *tray_data;
GSList *l, *objects;
ECalComponentId *our_id;
debug (("..."));
tray_data = msg->data;
objects = msg->objects;
our_id = e_cal_component_get_id (tray_data->comp);
g_return_if_fail (our_id);
for (l = objects; l != NULL; l = l->next) {
ECalComponentId *id = l->data;
if (!id)
continue;
if (tray_data &&
g_strcmp0 (id->uid, our_id->uid) == 0 &&
g_strcmp0 (id->rid, our_id->rid) == 0) {
tray_data->cqa = NULL;
tray_data->alarm_id = NULL;
tray_icons_list = g_list_remove (
tray_icons_list, tray_data);
tray_data = NULL;
}
e_cal_component_free_id (id);
}
e_cal_component_free_id (our_id);
g_slist_free (objects);
g_slice_free (struct _query_msg, msg);
}
static void
on_dialog_objs_removed_cb (ECalClientView *view,
const GSList *uids,
gpointer data)
{
struct _query_msg *msg;
msg = g_slice_new0 (struct _query_msg);
msg->header.func = (MessageFunc) on_dialog_objs_removed_async;
msg->objects = duplicate_ecal (uids);
msg->data = data;
message_push ((Message *) msg);
}
struct _tray_cqa_msg {
Message header;
CompQueuedAlarms *cqa;
};
static void
tray_list_remove_cqa_async (struct _tray_cqa_msg *msg)
{
CompQueuedAlarms *cqa = msg->cqa;
GList *list = tray_icons_list;
debug (("Removing CQA %p from tray list", cqa));
while (list) {
TrayIconData *tray_data = list->data;
GList *tmp = list;
GtkTreeModel *model;
list = list->next;
if (tray_data->cqa == cqa) {
debug (("Found"));
tray_icons_list = g_list_delete_link (tray_icons_list, tmp);
if (alarm_notifications_dialog && tray_data->is_in_tree) {
model = gtk_tree_view_get_model (
GTK_TREE_VIEW (alarm_notifications_dialog->treeview));
gtk_list_store_remove (GTK_LIST_STORE (model), &(tray_data->iter));
tray_data->is_in_tree = FALSE;
}
free_tray_icon_data (tray_data);
}
}
debug (("%d alarms left", g_list_length (tray_icons_list)));
if (alarm_notifications_dialog) {
if (!g_list_length (tray_icons_list)) {
gtk_widget_destroy (alarm_notifications_dialog->dialog);
g_free (alarm_notifications_dialog);
alarm_notifications_dialog = NULL;
} else {
GtkTreeIter iter;
GtkTreeModel *model;
GtkTreeSelection *sel;
model = gtk_tree_view_get_model (
GTK_TREE_VIEW (alarm_notifications_dialog->treeview));
if (gtk_tree_model_get_iter_first (model, &iter)) {
sel = gtk_tree_view_get_selection (
GTK_TREE_VIEW (alarm_notifications_dialog->treeview));
gtk_tree_selection_select_iter (sel, &iter);
}
}
}
g_slice_free (struct _tray_cqa_msg, msg);
}
static void
tray_list_remove_cqa (CompQueuedAlarms *cqa)
{
struct _tray_cqa_msg *msg;
msg = g_slice_new0 (struct _tray_cqa_msg);
msg->header.func = (MessageFunc) tray_list_remove_cqa_async;
msg->cqa = cqa;
message_push ((Message *) msg);
}
/* Callback used from the alarm notify dialog */
static void
tray_list_remove_async (Message *msg)
{
GList *list = tray_icons_list;
debug (("Removing %d alarms", g_list_length (list)));
while (list != NULL) {
TrayIconData *tray_data = list->data;
if (!tray_data->snooze_set) {
GList *temp = list->next;
gboolean status;
tray_icons_list = g_list_remove_link (tray_icons_list, list);
if (alarm_notifications_dialog && tray_data->is_in_tree) {
GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (alarm_notifications_dialog->treeview));
gtk_list_store_remove (GTK_LIST_STORE (model), &tray_data->iter);
tray_data->is_in_tree = FALSE;
}
status = remove_queued_alarm (
tray_data->cqa,
tray_data->alarm_id, FALSE, TRUE);
if (status) {
g_hash_table_remove (
tray_data->cqa->parent_client->uid_alarms_hash,
tray_data->cqa->id);
e_cal_component_free_id (tray_data->cqa->id);
g_free (tray_data->cqa);
}
free_tray_icon_data (tray_data);
tray_data = NULL;
g_list_free_1 (list);
if (tray_icons_list != list) /* List head is modified */
list = tray_icons_list;
else
list = temp;
} else
list = list->next;
}
g_slice_free (Message, msg);
}
static void
tray_list_remove_icons (void)
{
Message *msg;
msg = g_slice_new0 (Message);
msg->func = tray_list_remove_async;
message_push (msg);
}
struct _tray_msg {
Message header;
TrayIconData *data;
};
static void
tray_list_remove_data_async (struct _tray_msg *msg)
{
TrayIconData *tray_data = msg->data;
debug (("Removing %p from tray list", tray_data));
tray_icons_list = g_list_remove_all (tray_icons_list, tray_data);
free_tray_icon_data (tray_data);
tray_data = NULL;
g_slice_free (struct _tray_msg, msg);
}
static void
tray_list_remove_data (TrayIconData *data)
{
struct _tray_msg *msg;
msg = g_slice_new0 (struct _tray_msg);
msg->header.func = (MessageFunc) tray_list_remove_data_async;
msg->data = data;
message_push ((Message *) msg);
}
static void
notify_dialog_cb (AlarmNotifyResult result,
gint snooze_mins,
gpointer data)
{
TrayIconData *tray_data = data;
debug (("Received from dialog"));
g_signal_handlers_disconnect_matched (
tray_data->view, G_SIGNAL_MATCH_FUNC,
0, 0, NULL, on_dialog_objs_removed_cb, NULL);
switch (result) {
case ALARM_NOTIFY_SNOOZE:
debug (("Creating a snooze"));
create_snooze (tray_data->cqa, tray_data->alarm_id, snooze_mins);
tray_data->snooze_set = TRUE;
tray_list_remove_data (tray_data);
if (alarm_notifications_dialog) {
GtkTreeSelection *selection =
gtk_tree_view_get_selection (
GTK_TREE_VIEW (alarm_notifications_dialog->treeview));
GtkTreeIter iter;
GtkTreeModel *model = NULL;
/* We can also use tray_data->iter */
if (gtk_tree_selection_get_selected (selection, &model, &iter)) {
gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
tray_data->is_in_tree = FALSE;
if (!gtk_tree_model_get_iter_first (model, &iter)) {
/* We removed the last one */
gtk_widget_destroy (alarm_notifications_dialog->dialog);
g_free (alarm_notifications_dialog);
alarm_notifications_dialog = NULL;
} else {
/* Select the first */
gtk_tree_selection_select_iter (selection, &iter);
}
}
}
break;
case ALARM_NOTIFY_EDIT:
edit_component (tray_data->cal_client, tray_data->comp);
break;
case ALARM_NOTIFY_PRINT:
print_component (tray_data->cal_client, tray_data->comp);
break;
case ALARM_NOTIFY_DISMISS:
if (alarm_notifications_dialog && tray_data->is_in_tree) {
GtkTreeModel *model;
model = gtk_tree_view_get_model (
GTK_TREE_VIEW (alarm_notifications_dialog->treeview));
gtk_list_store_remove (GTK_LIST_STORE (model), &tray_data->iter);
tray_data->is_in_tree = FALSE;
}
break;
case ALARM_NOTIFY_CLOSE:
debug (("Dialog close"));
if (alarm_notifications_dialog) {
GtkTreeIter iter;
GtkTreeModel *model =
gtk_tree_view_get_model (
GTK_TREE_VIEW (alarm_notifications_dialog->treeview));
gboolean valid = gtk_tree_model_get_iter_first (model, &iter);
/* Maybe we should warn about this first? */
while (valid) {
valid = gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
}
gtk_widget_destroy (alarm_notifications_dialog->dialog);
g_free (alarm_notifications_dialog);
alarm_notifications_dialog = NULL;
/* Task to remove the tray icons */
tray_list_remove_icons ();
}
break;
default:
g_return_if_reached ();
}
return;
}
static void
remove_tray_icon (void)
{
if (tray_blink_id > -1)
g_source_remove (tray_blink_id);
tray_blink_id = -1;
if (tray_icon) {
gtk_status_icon_set_visible (tray_icon, FALSE);
g_object_unref (tray_icon);
tray_icon = NULL;
}
}
/* Callbacks. */
static gboolean
open_alarm_dialog (TrayIconData *tray_data)
{
QueuedAlarm *qa;
debug (("..."));
qa = lookup_queued_alarm (tray_data->cqa, tray_data->alarm_id);
if (qa) {
remove_tray_icon ();
if (!alarm_notifications_dialog)
alarm_notifications_dialog = notified_alarms_dialog_new ();
if (alarm_notifications_dialog) {
GtkTreeSelection *selection = NULL;
selection = gtk_tree_view_get_selection (
GTK_TREE_VIEW (alarm_notifications_dialog->treeview));
tray_data->iter = add_alarm_to_notified_alarms_dialog (
alarm_notifications_dialog,
tray_data->trigger,
qa->instance->occur_start,
qa->instance->occur_end,
e_cal_component_get_vtype (tray_data->comp),
tray_data->summary,
tray_data->description,
tray_data->location,
notify_dialog_cb, tray_data);
tray_data->is_in_tree = TRUE;
gtk_tree_selection_select_iter (
selection, &tray_data->iter);
gtk_window_present (GTK_WINDOW (alarm_notifications_dialog->dialog));
}
} else {
remove_tray_icon ();
}
return TRUE;
}
static gint
tray_icon_clicked_cb (GtkWidget *widget,
GdkEvent *event,
gpointer user_data)
{
if (event->type == GDK_BUTTON_PRESS) {
guint event_button = 0;
debug (("left click and %d alarms", g_list_length (tray_icons_list)));
gdk_event_get_button (event, &event_button);
if (event_button == 1 && g_list_length (tray_icons_list) > 0) {
GList *tmp;
for (tmp = tray_icons_list; tmp; tmp = tmp->next) {
open_alarm_dialog (tmp->data);
}
return TRUE;
} else if (event_button == 3) {
debug (("right click"));
remove_tray_icon ();
return TRUE;
}
}
return FALSE;
}
static void
icon_activated (GtkStatusIcon *icon)
{
GdkEvent event;
event.type = GDK_BUTTON_PRESS;
event.button.button = 1;
event.button.time = gtk_get_current_event_time ();
tray_icon_clicked_cb (NULL, &event, NULL);
}
static void
popup_menu (GtkStatusIcon *icon,
guint button,
guint activate_time)
{
if (button == 3) {
/* right click */
GdkEvent event;
event.type = GDK_BUTTON_PRESS;
event.button.button = 3;
event.button.time = gtk_get_current_event_time ();
tray_icon_clicked_cb (NULL, &event, NULL);
}
}
static gboolean
tray_icon_blink_cb (gpointer data)
{
static gboolean tray_blink_state = FALSE;
const gchar *icon_name;
tray_blink_countdown--;
tray_blink_state = !tray_blink_state;
if (tray_blink_state || tray_blink_countdown <= 0)
icon_name = "appointment-missed";
else
icon_name = "appointment-soon";
if (tray_icon)
gtk_status_icon_set_from_icon_name (tray_icon, icon_name);
if (tray_blink_countdown <= 0)
tray_blink_id = -1;
return tray_blink_countdown > 0;
}
/* Add a new data to tray list */
static void
tray_list_add_async (struct _tray_msg *msg)
{
tray_icons_list = g_list_prepend (tray_icons_list, msg->data);
g_slice_free (struct _tray_msg, msg);
}
static void
tray_list_add_new (TrayIconData *data)
{
struct _tray_msg *msg;
msg = g_slice_new0 (struct _tray_msg);
msg->header.func = (MessageFunc) tray_list_add_async;
msg->data = data;
message_push ((Message *) msg);
}
static void
alarm_queue_get_alarm_summary (ECalComponent *comp,
const ECalComponentAlarmInstance *instance,
ECalComponentText *text,
ECalComponentAlarm **palarm)
{
g_return_if_fail (comp != NULL);
g_return_if_fail (instance != NULL);
g_return_if_fail (instance->auid != NULL);
g_return_if_fail (text != NULL);
g_return_if_fail (palarm != NULL);
text->value = NULL;
*palarm = e_cal_component_get_alarm (comp, instance->auid);
if (*palarm) {
e_cal_component_alarm_get_description (*palarm, text);
if (!text->value || !*text->value) {
text->value = NULL;
e_cal_component_alarm_free (*palarm);
*palarm = NULL;
}
}
if (!text->value)
e_cal_component_get_summary (comp, text);
}
/* Performs notification of a display alarm */
static void
display_notification (time_t trigger,
CompQueuedAlarms *cqa,
gpointer alarm_id,
gboolean use_description)
{
QueuedAlarm *qa;
ECalComponent *comp;
ECalComponentAlarm *comp_alarm = NULL;
const gchar *summary, *description, *location;
TrayIconData *tray_data;
ECalComponentText text;
GSList *text_list;
gchar *str, *start_str, *end_str, *alarm_str, *time_str;
icaltimezone *current_zone;
ECalComponentOrganizer organiser;
debug (("..."));
comp = cqa->alarms->comp;
qa = lookup_queued_alarm (cqa, alarm_id);
if (!qa)
return;
/* get a sensible description for the event */
alarm_queue_get_alarm_summary (comp, qa->instance, &text, &comp_alarm);
e_cal_component_get_organizer (comp, &organiser);
if (text.value)
summary = text.value;
else
summary = _("No summary available.");
e_cal_component_get_description_list (comp, &text_list);
if (text_list) {
text = *((ECalComponentText *) text_list->data);
if (text.value)
description = text.value;
else
description = _("No description available.");
} else {
description = _("No description available.");
}
e_cal_component_free_text_list (text_list);
e_cal_component_get_location (comp, &location);
if (!location)
location = _("No location information available.");
/* create the tray icon */
if (tray_icon == NULL) {
tray_icon = gtk_status_icon_new ();
gtk_status_icon_set_title (tray_icon, _("Evolution Reminders"));
gtk_status_icon_set_from_icon_name (
tray_icon, "appointment-soon");
g_signal_connect (
tray_icon, "activate",
G_CALLBACK (icon_activated), NULL);
g_signal_connect (
tray_icon, "popup-menu",
G_CALLBACK (popup_menu), NULL);
}
current_zone = config_data_get_timezone ();
alarm_str = timet_to_str_with_zone (trigger, current_zone);
start_str = timet_to_str_with_zone (qa->instance->occur_start, current_zone);
end_str = timet_to_str_with_zone (qa->instance->occur_end, current_zone);
time_str = calculate_time (qa->instance->occur_start, qa->instance->occur_end);
str = g_strdup_printf (
"%s\n%s %s",
summary, start_str, time_str);
/* create the private structure */
tray_data = g_new0 (TrayIconData, 1);
tray_data->summary = g_strdup (summary);
tray_data->description = g_strdup (description);
tray_data->location = g_strdup (location);
tray_data->trigger = trigger;
tray_data->cqa = cqa;
tray_data->alarm_id = alarm_id;
tray_data->comp = e_cal_component_clone (comp);
tray_data->cal_client = cqa->parent_client->cal_client;
tray_data->view = g_object_ref (cqa->parent_client->view);
tray_data->blink_state = FALSE;
tray_data->snooze_set = FALSE;
tray_data->is_in_tree = FALSE;
g_object_ref (tray_data->cal_client);
/* Task to add tray_data to the global tray_icon_list */
tray_list_add_new (tray_data);
if (g_list_length (tray_icons_list) > 1) {
gchar *tip;
tip = g_strdup_printf (ngettext (
"You have %d reminder", "You have %d reminders",
g_list_length (tray_icons_list)),
g_list_length (tray_icons_list));
gtk_status_icon_set_tooltip_text (tray_icon, tip);
}
else {
gtk_status_icon_set_tooltip_text (tray_icon, str);
}
if (comp_alarm)
e_cal_component_alarm_free (comp_alarm);
g_free (start_str);
g_free (end_str);
g_free (alarm_str);
g_free (time_str);
g_free (str);
g_signal_connect (
tray_data->view, "objects_removed",
G_CALLBACK (on_dialog_objs_removed_cb), tray_data);
/* FIXME: We should remove this check */
if (!config_data_get_notify_with_tray ()) {
tray_blink_id = -1;
open_alarm_dialog (tray_data);
if (alarm_notifications_dialog)
gtk_window_stick (GTK_WINDOW (
alarm_notifications_dialog->dialog));
} else {
if (tray_blink_id == -1) {
tray_blink_countdown = 30;
tray_blink_id = g_timeout_add (500, tray_icon_blink_cb, tray_data);
}
}
}
#ifdef HAVE_LIBNOTIFY
static void
popup_notification (time_t trigger,
CompQueuedAlarms *cqa,
gpointer alarm_id,
gboolean use_description)
{
QueuedAlarm *qa;
ECalComponent *comp;
ECalComponentAlarm *comp_alarm = NULL;
const gchar *summary, *location;
ECalComponentText text;
gchar *str, *start_str, *end_str, *alarm_str, *time_str;
icaltimezone *current_zone;
ECalComponentOrganizer organiser;
NotifyNotification *notify;
gchar *body;
debug (("..."));
comp = cqa->alarms->comp;
qa = lookup_queued_alarm (cqa, alarm_id);
if (!qa)
return;
if (!notify_is_initted ())
notify_init (_("Evolution Reminders"));
/* get a sensible description for the event */
alarm_queue_get_alarm_summary (comp, qa->instance, &text, &comp_alarm);
e_cal_component_get_organizer (comp, &organiser);
if (text.value)
summary = text.value;
else
summary = _("No summary available.");
e_cal_component_get_location (comp, &location);
/* create the tray icon */
current_zone = config_data_get_timezone ();
alarm_str = timet_to_str_with_zone (trigger, current_zone);
start_str = timet_to_str_with_zone (qa->instance->occur_start, current_zone);
end_str = timet_to_str_with_zone (qa->instance->occur_end, current_zone);
time_str = calculate_time (qa->instance->occur_start, qa->instance->occur_end);
str = g_strdup_printf (
"%s %s",
start_str, time_str);
if (organiser.cn) {
if (location)
body = g_strdup_printf (
"<b>%s</b>\n%s %s\n%s %s",
organiser.cn, _("Location:"),
location, start_str, time_str);
else
body = g_strdup_printf (
"<b>%s</b>\n%s %s",
organiser.cn, start_str, time_str);
}
else {
if (location)
body = g_strdup_printf (
"%s %s\n%s %s", _("Location:"),
location, start_str, time_str);
else
body = g_strdup_printf (
"%s %s", start_str, time_str);
}
notify = notify_notification_new (summary, body, "appointment-soon");
/* If the user wants Evolution notifications suppressed, honor
* it even though evolution-alarm-notify is a separate process
* with its own .desktop file. */
notify_notification_set_hint (
notify, "desktop-entry",
g_variant_new_string (PACKAGE));
if (!notify_notification_show (notify, NULL))
g_warning ("Could not send notification to daemon\n");
if (comp_alarm)
e_cal_component_alarm_free (comp_alarm);
/* create the private structure */
g_free (start_str);
g_free (end_str);
g_free (alarm_str);
g_free (time_str);
g_free (str);
}
#endif
/* Performs notification of an audio alarm */
static void
audio_notification (time_t trigger,
CompQueuedAlarms *cqa,
gpointer alarm_id)
{
QueuedAlarm *qa;
ECalComponent *comp;
ECalComponentAlarm *alarm;
icalattach *attach;
gint flag = 0;
debug (("..."));
comp = cqa->alarms->comp;
qa = lookup_queued_alarm (cqa, alarm_id);
if (!qa)
return;
alarm = e_cal_component_get_alarm (comp, qa->instance->auid);
g_return_if_fail (alarm != NULL);
e_cal_component_alarm_get_attach (alarm, &attach);
e_cal_component_alarm_free (alarm);
if (attach && icalattach_get_is_url (attach)) {
const gchar *url;
url = icalattach_get_url (attach);
if (url && *url) {
gchar *filename;
GError *error = NULL;
filename = g_filename_from_uri (url, NULL, &error);
if (error != NULL) {
g_warning ("%s: %s", G_STRFUNC, error->message);
g_error_free (error);
} else if (filename && g_file_test (filename, G_FILE_TEST_EXISTS)) {
#ifdef HAVE_CANBERRA
flag = 1;
ca_context_play (
ca_gtk_context_get (), 0,
CA_PROP_MEDIA_FILENAME, filename, NULL);
#endif
}
g_free (filename);
}
}
if (!flag)
gdk_beep ();
if (attach)
icalattach_unref (attach);
}
/* Performs notification of a mail alarm */
static void
mail_notification (time_t trigger,
CompQueuedAlarms *cqa,
gpointer alarm_id)
{
if (!e_client_check_capability (
E_CLIENT (cqa->parent_client->cal_client),
CAL_STATIC_CAPABILITY_NO_EMAIL_ALARMS))
return;
/* FIXME Implement this. */
}
/* Performs notification of a procedure alarm */
static gboolean
procedure_notification_dialog (const gchar *cmd,
const gchar *url)
{
GtkWidget *container;
GtkWidget *dialog;
GtkWidget *label;
GtkWidget *checkbox;
gchar *str;
gint btn;
debug (("..."));
if (config_data_is_blessed_program (url))
return TRUE;
dialog = gtk_dialog_new_with_buttons (
_("Warning"), NULL, 0,
GTK_STOCK_NO, GTK_RESPONSE_CANCEL,
GTK_STOCK_YES, GTK_RESPONSE_OK,
NULL);
str = g_strdup_printf (
_("An Evolution Calendar reminder is about to trigger. "
"This reminder is configured to run the following program:\n\n"
" %s\n\n"
"Are you sure you want to run this program?"),
cmd);
label = gtk_label_new (str);
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_LEFT);
gtk_widget_show (label);
container = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
gtk_box_pack_start (GTK_BOX (container), label, TRUE, TRUE, 4);
g_free (str);
checkbox = gtk_check_button_new_with_label
(_("Do not ask me about this program again."));
gtk_widget_show (checkbox);
gtk_box_pack_start (GTK_BOX (container), checkbox, TRUE, TRUE, 4);
/* Run the dialog */
btn = gtk_dialog_run (GTK_DIALOG (dialog));
if (btn == GTK_RESPONSE_OK &&
gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (checkbox)))
config_data_save_blessed_program (url);
gtk_widget_destroy (dialog);
return (btn == GTK_RESPONSE_OK);
}
static void
procedure_notification (time_t trigger,
CompQueuedAlarms *cqa,
gpointer alarm_id)
{
QueuedAlarm *qa;
ECalComponent *comp;
ECalComponentAlarm *alarm;
ECalComponentText description;
icalattach *attach;
const gchar *url;
gchar *cmd;
gboolean result = TRUE;
debug (("..."));
comp = cqa->alarms->comp;
qa = lookup_queued_alarm (cqa, alarm_id);
if (!qa)
return;
alarm = e_cal_component_get_alarm (comp, qa->instance->auid);
g_return_if_fail (alarm != NULL);
e_cal_component_alarm_get_attach (alarm, &attach);
e_cal_component_alarm_get_description (alarm, &description);
e_cal_component_alarm_free (alarm);
/* If the alarm has no attachment, simply display a notification dialog. */
if (!attach)
goto fallback;
if (!icalattach_get_is_url (attach)) {
icalattach_unref (attach);
goto fallback;
}
url = icalattach_get_url (attach);
g_return_if_fail (url != NULL);
/* Ask for confirmation before executing the stuff */
if (description.value)
cmd = g_strconcat (url, " ", description.value, NULL);
else
cmd = (gchar *) url;
if (procedure_notification_dialog (cmd, url))
result = g_spawn_command_line_async (cmd, NULL);
if (cmd != (gchar *) url)
g_free (cmd);
icalattach_unref (attach);
/* Fall back to display notification if we got an error */
if (result == FALSE)
goto fallback;
return;
fallback:
display_notification (trigger, cqa, alarm_id, FALSE);
}
static gboolean
check_midnight_refresh (gpointer user_data)
{
time_t new_midnight;
icaltimezone *zone;
debug (("..."));
zone = config_data_get_timezone ();
new_midnight = time_day_end_with_zone (time (NULL), zone);
if (new_midnight > midnight) {
struct _midnight_refresh_msg *msg;
msg = g_slice_new0 (struct _midnight_refresh_msg);
msg->header.func = (MessageFunc) midnight_refresh_async;
msg->remove = FALSE;
message_push ((Message *) msg);
}
return TRUE;
}
static gboolean
check_wall_clock_time_changed (gpointer user_data)
{
static gint64 expected_wall_clock_time = 0;
gint64 wall_clock_time;
#define ADD_SECONDS(to, secs) ((to) + ((secs) * 1000000))
wall_clock_time = g_get_real_time ();
/* use one second margin */
if (wall_clock_time > ADD_SECONDS (expected_wall_clock_time, 1) ||
wall_clock_time < ADD_SECONDS (expected_wall_clock_time, -1)) {
debug (("Current wall-clock time differs from expected, rescheduling alarms"));
check_midnight_refresh (NULL);
alarm_reschedule_timeout ();
}
expected_wall_clock_time = ADD_SECONDS (wall_clock_time, 60);
#undef ADD_SECONDS
return TRUE;
}
/**
* alarm_queue_init:
*
* Initializes the alarm queueing system. This should be called near the
* beginning of the program.
**/
void
alarm_queue_init (gpointer data)
{
an = data;
g_return_if_fail (alarm_queue_inited == FALSE);
debug (("..."));
client_alarms_hash = g_hash_table_new (g_direct_hash, g_direct_equal);
queue_midnight_refresh ();
if (config_data_get_last_notification_time (NULL) == -1) {
time_t tmval = time_day_begin (time (NULL));
debug (("Setting last notification time to %s", e_ctime (&tmval)));
config_data_set_last_notification_time (NULL, tmval);
}
/* install timeout handler (every 30 mins) for not missing the midnight refresh */
g_timeout_add_seconds (1800, check_midnight_refresh, NULL);
/* monotonic time doesn't change during hibernation, while the wall clock time does,
* thus check for wall clock time changes and reschedule alarms when it changes */
g_timeout_add_seconds (60, check_wall_clock_time_changed, NULL);
#ifdef HAVE_LIBNOTIFY
notify_init (_("Evolution Reminders"));
#endif
alarm_queue_inited = TRUE;
}
static gboolean
free_client_alarms_cb (gpointer key,
gpointer value,
gpointer user_data)
{
ClientAlarms *ca = value;
debug (("ca=%p", ca));
if (ca) {
remove_client_alarms (ca);
if (ca->cal_client) {
debug (("Disconnecting Client"));
g_signal_handlers_disconnect_matched (
ca->cal_client, G_SIGNAL_MATCH_DATA,
0, 0, NULL, NULL, ca);
g_object_unref (ca->cal_client);
}
if (ca->view) {
debug (("Disconnecting Query"));
g_signal_handlers_disconnect_matched (
ca->view, G_SIGNAL_MATCH_DATA,
0, 0, NULL, NULL, ca);
g_object_unref (ca->view);
}
g_hash_table_destroy (ca->uid_alarms_hash);
g_free (ca);
return TRUE;
}
return FALSE;
}
/**
* alarm_queue_done:
*
* Shuts down the alarm queueing system. This should be called near the end
* of the program. All the monitored calendar clients should already have been
* unregistered with alarm_queue_remove_client().
**/
void
alarm_queue_done (void)
{
g_return_if_fail (alarm_queue_inited);
/* All clients must be unregistered by now */
g_return_if_fail (g_hash_table_size (client_alarms_hash) == 0);
debug (("..."));
g_hash_table_foreach_remove (
client_alarms_hash, (GHRFunc) free_client_alarms_cb, NULL);
g_hash_table_destroy (client_alarms_hash);
client_alarms_hash = NULL;
if (midnight_refresh_id != NULL) {
alarm_remove (midnight_refresh_id);
midnight_refresh_id = NULL;
}
alarm_queue_inited = FALSE;
}
static gboolean
compare_ids (gpointer a,
gpointer b)
{
ECalComponentId *id, *id1;
id = a;
id1 = b;
if (id->uid != NULL && id1->uid != NULL) {
if (g_str_equal (id->uid, id1->uid)) {
if (id->rid && id1->rid)
return g_str_equal (id->rid, id1->rid);
else if (!(id->rid && id1->rid))
return TRUE;
}
}
return FALSE;
}
static guint
hash_ids (gpointer a)
{
ECalComponentId *id =a;
return g_str_hash (id->uid);
}
struct _alarm_client_msg {
Message header;
ECalClient *cal_client;
};
static void
alarm_queue_add_async (struct _alarm_client_msg *msg)
{
ClientAlarms *ca;
ECalClient *cal_client = msg->cal_client;
g_return_if_fail (alarm_queue_inited);
g_return_if_fail (cal_client != NULL);
g_return_if_fail (E_IS_CAL_CLIENT (cal_client));
ca = lookup_client (cal_client);
if (ca) {
/* We already have it. Unref the passed one*/
g_object_unref (cal_client);
return;
}
debug (("client=%p", cal_client));
ca = g_new (ClientAlarms, 1);
ca->cal_client = cal_client;
ca->view = NULL;
g_hash_table_insert (client_alarms_hash, cal_client, ca);
ca->uid_alarms_hash = g_hash_table_new (
(GHashFunc) hash_ids, (GEqualFunc) compare_ids);
load_alarms_for_today (ca);
g_slice_free (struct _alarm_client_msg, msg);
}
/**
* alarm_queue_add_client:
* @cal_client: A calendar client.
*
* Adds a calendar client to the alarm queueing system. Alarm trigger
* notifications will be presented at the appropriate times. The client should
* be removed with alarm_queue_remove_client() when receiving notifications
* from it is no longer desired.
*
* A client can be added any number of times to the alarm queueing system,
* but any single alarm trigger will only be presented once for a particular
* client. The client must still be removed the same number of times from the
* queueing system when it is no longer wanted.
**/
void
alarm_queue_add_client (ECalClient *cal_client)
{
struct _alarm_client_msg *msg;
msg = g_slice_new0 (struct _alarm_client_msg);
msg->header.func = (MessageFunc) alarm_queue_add_async;
msg->cal_client = g_object_ref (cal_client);
message_push ((Message *) msg);
}
/* Removes a component an its alarms */
static void
remove_cqa (ClientAlarms *ca,
ECalComponentId *id,
CompQueuedAlarms *cqa)
{
/* If a component is present, then it means we must have alarms queued
* for it.
*/
g_return_if_fail (cqa->queued_alarms != NULL);
debug (("removing %d alarms", g_slist_length (cqa->queued_alarms)));
remove_alarms (cqa, TRUE);
}
static gboolean
remove_comp_by_id (gpointer key,
gpointer value,
gpointer userdata)
{
ClientAlarms *ca = (ClientAlarms *) userdata;
debug (("..."));
/* if (!g_hash_table_size (ca->uid_alarms_hash)) */
/* return; */
remove_cqa (ca, (ECalComponentId *) key, (CompQueuedAlarms *) value);
return TRUE;
}
/* Removes all the alarms queued for a particular calendar client */
static void
remove_client_alarms (ClientAlarms *ca)
{
debug (("size %d", g_hash_table_size (ca->uid_alarms_hash)));
g_hash_table_foreach_remove (
ca->uid_alarms_hash, (GHRFunc) remove_comp_by_id, ca);
/* The hash table should be empty now */
g_return_if_fail (g_hash_table_size (ca->uid_alarms_hash) == 0);
}
/**
* alarm_queue_remove_client:
* @client: A calendar client.
*
* Removes a calendar client from the alarm queueing system.
**/
static void
alarm_queue_remove_async (struct _alarm_client_msg *msg)
{
ClientAlarms *ca;
ECalClient *cal_client = msg->cal_client;
g_return_if_fail (alarm_queue_inited);
g_return_if_fail (cal_client != NULL);
g_return_if_fail (E_IS_CAL_CLIENT (cal_client));
ca = lookup_client (cal_client);
g_return_if_fail (ca != NULL);
debug (("..."));
remove_client_alarms (ca);
/* Clean up */
if (ca->cal_client) {
debug (("Disconnecting Client"));
g_signal_handlers_disconnect_matched (
ca->cal_client, G_SIGNAL_MATCH_DATA,
0, 0, NULL, NULL, ca);
g_object_unref (ca->cal_client);
ca->cal_client = NULL;
}
if (ca->view) {
debug (("Disconnecting Query"));
g_signal_handlers_disconnect_matched (
ca->view, G_SIGNAL_MATCH_DATA,
0, 0, NULL, NULL, ca);
g_object_unref (ca->view);
ca->view = NULL;
}
g_hash_table_destroy (ca->uid_alarms_hash);
ca->uid_alarms_hash = NULL;
g_free (ca);
g_hash_table_remove (client_alarms_hash, cal_client);
g_slice_free (struct _alarm_client_msg, msg);
}
/** alarm_queue_remove_client
*
* asynchronously remove client from alarm queue.
* @cal_client: Client to remove.
* @immediately: Indicates whether use thread or do it right now.
*/
void
alarm_queue_remove_client (ECalClient *cal_client,
gboolean immediately)
{
struct _alarm_client_msg *msg;
msg = g_slice_new0 (struct _alarm_client_msg);
msg->header.func = (MessageFunc) alarm_queue_remove_async;
msg->cal_client = cal_client;
if (immediately) {
alarm_queue_remove_async (msg);
} else
message_push ((Message *) msg);
}
/* Update non-time related variables for various structures on modification
* of an existing component to be called only from query_objects_changed_cb */
static void
update_cqa (CompQueuedAlarms *cqa,
ECalComponent *newcomp)
{
ECalComponent *oldcomp;
ECalComponentAlarms *alarms = NULL;
GSList *qa_list; /* List of current QueuedAlarms corresponding to cqa */
time_t from, to;
icaltimezone *zone;
ECalComponentAlarmAction omit[] = {-1};
oldcomp = cqa->alarms->comp;
zone = config_data_get_timezone ();
from = time_day_begin_with_zone (time (NULL), zone);
to = time_day_end_with_zone (time (NULL), zone);
debug (("Generating alarms between %s and %s", e_ctime (&from), e_ctime (&to)));
alarms = e_cal_util_generate_alarms_for_comp (
newcomp, from, to, omit, e_cal_client_resolve_tzid_cb,
cqa->parent_client->cal_client, zone);
/* Update auids in Queued Alarms*/
for (qa_list = cqa->queued_alarms; qa_list; qa_list = qa_list->next) {
QueuedAlarm *qa = qa_list->data;
gchar *check_auid = (gchar *) qa->instance->auid;
ECalComponentAlarm *alarm;
alarm = e_cal_component_get_alarm (newcomp, check_auid);
if (alarm) {
e_cal_component_alarm_free (alarm);
continue;
} else {
alarm = e_cal_component_get_alarm (oldcomp, check_auid);
if (alarm) { /* Need to update QueuedAlarms */
e_cal_component_alarm_free (alarm);
if (alarms == NULL) {
debug (("No alarms found in the modified component"));
break;
}
update_qa (alarms, qa);
}
else
g_warning ("Failed in auid lookup for old component also\n");
}
}
/* Update the actual component stored in CompQueuedAlarms structure */
g_object_unref (cqa->alarms->comp);
cqa->alarms->comp = newcomp;
if (alarms != NULL)
e_cal_component_alarms_free (alarms);
}
static void
update_qa (ECalComponentAlarms *alarms,
QueuedAlarm *qa)
{
ECalComponentAlarmInstance *al_inst;
GSList *instance_list;
debug (("..."));
for (instance_list = alarms->alarms;
instance_list;
instance_list = instance_list->next) {
al_inst = instance_list->data;
/* FIXME If two or more alarm instances (audio, note)
* for same component have same trigger... */
if (al_inst->trigger == qa->orig_trigger) {
g_free ((gchar *) qa->instance->auid);
qa->instance->auid = g_strdup (al_inst->auid);
break;
}
}
}