/* * 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 * * * Authors: * Federico Mena-Quintero * * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) * */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #ifdef HAVE_CANBERRA #include #endif #ifdef HAVE_LIBNOTIFY #include #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 (ECalClient *cal_client, 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; if (e_client_check_capability (E_CLIENT (cal_client), "alarm-description")) { *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; } } else { *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 (cqa->parent_client->cal_client, 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 (cqa->parent_client->cal_client, 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 ( "%s\n%s %s\n%s %s", organiser.cn, _("Location:"), location, start_str, time_str); else body = g_strdup_printf ( "%s\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; } } }