alarm-queue.c 61.6 KB
Newer Older
1 2
/*
 * Alarm queueing engine
3
 *
4 5 6
 * 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.
7
 *
8 9 10 11
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
12
 *
13 14
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
15 16 17 18 19 20
 *
 *
 * Authors:
 *		Federico Mena-Quintero <federico@ximian.com>
 *
 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
21 22 23 24 25 26 27
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

Rodrigo Moya's avatar
Rodrigo Moya committed
28
#include <string.h>
29
#include <gtk/gtk.h>
Matthew Barnes's avatar
Matthew Barnes committed
30
#include <glib/gi18n.h>
31 32

#ifdef HAVE_CANBERRA
33
#include <canberra-gtk.h>
34
#endif
35

36 37 38 39
#ifdef HAVE_LIBNOTIFY
#include <libnotify/notify.h>
#endif

40
#include "alarm.h"
41
#include "alarm-notify-dialog.h"
42
#include "alarm-queue.h"
43
#include "alarm-notify.h"
44
#include "config-data.h"
45
#include "util.h"
46

47 48
#include "calendar/gui/print.h"

49 50 51
/* The dialog with alarm nofications */
static AlarmNotificationsDialog *alarm_notifications_dialog = NULL;

52
/* Whether the queueing system has been initialized */
53
static gboolean alarm_queue_inited = FALSE;
54 55 56 57

/* Clients we are monitoring for alarms */
static GHashTable *client_alarms_hash = NULL;

58 59 60
/* List of tray icons being displayed */
static GList *tray_icons_list = NULL;

61
/* Top Tray Image */
62
static GtkStatusIcon *tray_icon = NULL;
63 64
static gint tray_blink_id = -1;
static gint tray_blink_countdown = 0;
65 66
static AlarmNotify *an;

67 68 69
/* Structure that stores a client we are monitoring */
typedef struct {
	/* Monitored client */
70
	ECalClient *cal_client;
71

72 73
	/* The live view to the calendar */
	ECalClientView *view;
74

75 76 77 78 79 80 81 82 83
	/* 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;

84
/* Pair of a ECalComponentAlarms and the mapping from queued alarm IDs to the
85 86 87 88 89 90
 * actual alarm instance structures.
 */
typedef struct {
	/* The parent client alarms structure */
	ClientAlarms *parent_client;

Rodrigo Moya's avatar
Rodrigo Moya committed
91
	/* The component's UID */
92
	ECalComponentId *id;
Rodrigo Moya's avatar
Rodrigo Moya committed
93

94
	/* The actual component and its alarm instances */
95
	ECalComponentAlarms *alarms;
96 97 98

	/* List of QueuedAlarm structures */
	GSList *queued_alarms;
Rodrigo Moya's avatar
Rodrigo Moya committed
99 100 101

	/* Flags */
	gboolean expecting_update;
102 103 104 105 106 107 108
} 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;

109
	/* Instance from our parent CompQueuedAlarms->alarms->alarms list */
110
	ECalComponentAlarmInstance *instance;
111

Chenthill Palanisamy's avatar
Chenthill Palanisamy committed
112 113 114
	/* original trigger of the instance from component */
	time_t orig_trigger;

115 116 117 118
	#ifdef HAVE_LIBNOTIFY
	NotifyNotification *notify;
	#endif

119 120
	/* Whether this is a snoozed queued alarm or a normal one */
	guint snooze : 1;
121 122 123 124
} QueuedAlarm;

/* Alarm ID for the midnight refresh function */
static gpointer midnight_refresh_id = NULL;
Rodrigo Moya's avatar
Rodrigo Moya committed
125
static time_t midnight = 0;
126

127 128 129 130 131 132 133 134 135 136 137 138 139 140
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);
141
#ifdef HAVE_LIBNOTIFY
142 143 144 145
static void	popup_notification		(time_t trigger,
						 CompQueuedAlarms *cqa,
						 gpointer alarm_id,
						 gboolean use_description);
146
#endif
147 148
static void	query_objects_modified_cb	(ECalClientView *view,
						 const GSList *objects,
149
						 gpointer data);
150 151
static void	query_objects_removed_cb	(ECalClientView *view,
						 const GSList *uids,
152
						 gpointer data);
153

154 155 156 157 158 159 160 161
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);
162

163 164
/* Alarm queue engine */

165 166 167 168
static void	load_alarms_for_today		(ClientAlarms *ca);
static void	midnight_refresh_cb		(gpointer alarm_id,
						 time_t trigger,
						 gpointer data);
169

Matthew Barnes's avatar
Matthew Barnes committed
170 171 172 173 174 175 176 177 178
/* Simple asynchronous message dispatcher */

typedef struct _Message Message;
typedef void (*MessageFunc) (Message *msg);

struct _Message {
	MessageFunc func;
};

179
/*
Matthew Barnes's avatar
Matthew Barnes committed
180 181 182 183
static void
message_proxy (Message *msg)
{
	g_return_if_fail (msg->func != NULL);
184
 *
Matthew Barnes's avatar
Matthew Barnes committed
185 186
	msg->func (msg);
}
187
 *
Matthew Barnes's avatar
Matthew Barnes committed
188 189 190 191
static gpointer
create_thread_pool (void)
{
	return g_thread_pool_new ((GFunc) message_proxy, NULL, 1, FALSE, NULL);
192
}*/
Matthew Barnes's avatar
Matthew Barnes committed
193 194 195 196

static void
message_push (Message *msg)
{
197 198 199 200 201
	/* 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. */
202
	msg->func (msg);
Matthew Barnes's avatar
Matthew Barnes committed
203 204
}

205 206 207 208
/*
 * use a static ring-buffer so we can call this twice
 * in a printf without getting nonsense results.
 */
209
static const gchar *
210 211
e_ctime (const time_t *timep)
{
212 213 214 215 216 217 218 219
	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]);
220 221 222
		while (len > 0 && (buffer[next - 1][len - 1] == '\n' ||
			buffer[next - 1][len - 1] == '\r' ||
			g_ascii_isspace (buffer[next - 1][len - 1])))
223
			len--;
224

225 226
		buffer[next - 1][len - 1] = 0;
	}
227

228 229 230 231
	if (next >= G_N_ELEMENTS (buffer))
		next = 0;

	return ret;
232 233
}

234 235
/* Queues an alarm trigger for midnight so that we can load the next
 * day's worth of alarms. */
236 237 238
static void
queue_midnight_refresh (void)
{
239
	icaltimezone *zone;
240

Rodrigo Moya's avatar
Rodrigo Moya committed
241 242 243 244
	if (midnight_refresh_id != NULL) {
		alarm_remove (midnight_refresh_id);
		midnight_refresh_id = NULL;
	}
245

246 247
	zone = config_data_get_timezone ();
	midnight = time_day_end_with_zone (time (NULL), zone);
248

249
	debug (("Refresh at %s", e_ctime (&midnight)));
250

251 252
	midnight_refresh_id = alarm_add (
		midnight, midnight_refresh_cb, NULL, NULL);
253
	if (!midnight_refresh_id) {
254
		debug (("Could not setup the midnight refresh alarm"));
255 256 257 258 259 260
		/* FIXME: what to do? */
	}
}

/* Loads a client's alarms; called from g_hash_table_foreach() */
static void
261 262 263
add_client_alarms_cb (gpointer key,
                      gpointer value,
                      gpointer data)
264
{
265
	ClientAlarms *ca = (ClientAlarms *) value;
266

267
	debug (("Adding %p", ca));
268

Federico Mena Quintero's avatar
Federico Mena Quintero committed
269
	load_alarms_for_today (ca);
270 271
}

272
struct _midnight_refresh_msg {
Matthew Barnes's avatar
Matthew Barnes committed
273
	Message header;
274 275 276
	gboolean remove;
};

277 278
/* Loads the alarms for the new day every midnight */
static void
Matthew Barnes's avatar
Matthew Barnes committed
279
midnight_refresh_async (struct _midnight_refresh_msg *msg)
280
{
281
	debug (("..."));
282

283
	/* Re-load the alarms for all clients */
284 285 286
	g_hash_table_foreach (client_alarms_hash, add_client_alarms_cb, NULL);

	/* Re-schedule the midnight update */
Matthew Barnes's avatar
Matthew Barnes committed
287
	if (msg->remove && midnight_refresh_id != NULL) {
288
		debug (("Reschedule the midnight update"));
Rodrigo Moya's avatar
Rodrigo Moya committed
289 290 291 292
		alarm_remove (midnight_refresh_id);
		midnight_refresh_id = NULL;
	}

293
	queue_midnight_refresh ();
Matthew Barnes's avatar
Matthew Barnes committed
294 295

	g_slice_free (struct _midnight_refresh_msg, msg);
296 297
}

298
static void
299 300 301
midnight_refresh_cb (gpointer alarm_id,
                     time_t trigger,
                     gpointer data)
302
{
Matthew Barnes's avatar
Matthew Barnes committed
303
	struct _midnight_refresh_msg *msg;
304

Matthew Barnes's avatar
Matthew Barnes committed
305 306 307
	msg = g_slice_new0 (struct _midnight_refresh_msg);
	msg->header.func = (MessageFunc) midnight_refresh_async;
	msg->remove = TRUE;
308

Matthew Barnes's avatar
Matthew Barnes committed
309
	message_push ((Message *) msg);
310 311
}

312 313
/* Looks up a client in the client alarms hash table */
static ClientAlarms *
314
lookup_client (ECalClient *cal_client)
315
{
316
	return g_hash_table_lookup (client_alarms_hash, cal_client);
317 318
}

319 320
/* Looks up a queued alarm based on its alarm ID */
static QueuedAlarm *
321 322
lookup_queued_alarm (CompQueuedAlarms *cqa,
                     gpointer alarm_id)
323
{
324 325 326 327 328 329 330 331
	GSList *l;
	QueuedAlarm *qa;

	qa = NULL;

	for (l = cqa->queued_alarms; l; l = l->next) {
		qa = l->data;
		if (qa->alarm_id == alarm_id)
Rodrigo Moya's avatar
Rodrigo Moya committed
332
			return qa;
333 334
	}

Rodrigo Moya's avatar
Rodrigo Moya committed
335 336
	/* not found, might have been updated/removed */
	return NULL;
337 338
}

339 340 341 342 343 344 345 346 347 348
static void
alarm_queue_discard_alarm_cb (GObject *source,
			      GAsyncResult *result,
			      gpointer user_data)
{
	ECalClient *client = E_CAL_CLIENT (source);
	GError *error = NULL;

	g_return_if_fail (client != NULL);

349 350
	if (!e_cal_client_discard_alarm_finish (client, result, &error) &&
	    !g_error_matches (error, E_CLIENT_ERROR, E_CLIENT_ERROR_NOT_SUPPORTED))
351 352 353 354 355 356 357
		g_warning ("Failed to discard alarm at '%s': %s",
			e_source_get_display_name (e_client_get_source (E_CLIENT (client))),
			error ? error->message : "Unknown error");

	g_clear_error (&error);
}

358 359 360
/* 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.
 */
361
static gboolean
362 363 364 365
remove_queued_alarm (CompQueuedAlarms *cqa,
                     gpointer alarm_id,
                     gboolean free_object,
                     gboolean remove_alarm)
366
{
367
	QueuedAlarm *qa = NULL;
368
	GSList *l;
369

370
	debug (("..."));
371 372 373 374 375 376 377

	for (l = cqa->queued_alarms; l; l = l->next) {
		qa = l->data;
		if (qa->alarm_id == alarm_id)
			break;
	}

Rodrigo Moya's avatar
Rodrigo Moya committed
378
	if (!l)
379
		return FALSE;
380

381
	cqa->queued_alarms = g_slist_delete_link (cqa->queued_alarms, l);
382

383
	if (remove_alarm && !e_client_is_readonly (E_CLIENT (cqa->parent_client->cal_client))) {
384 385 386
		ECalComponentId *id;

		id = e_cal_component_get_id (cqa->alarms->comp);
387 388
		if (id) {
			cqa->expecting_update = TRUE;
389
			e_cal_client_discard_alarm (
390
				cqa->parent_client->cal_client, id->uid,
391 392
				id->rid, qa->instance->auid, NULL,
				alarm_queue_discard_alarm_cb, NULL);
393 394 395 396
			cqa->expecting_update = FALSE;

			e_cal_component_free_id (id);
		}
Rodrigo Moya's avatar
Rodrigo Moya committed
397 398
	}

399 400 401 402 403 404
	#ifdef HAVE_LIBNOTIFY
	if (qa->notify) {
		notify_notification_close (qa->notify, NULL);
		g_clear_object (&qa->notify);
	}
	#endif
405 406 407 408 409 410 411
	g_free (qa);

	/* If this was the last queued alarm for this component, remove the
	 * component itself.
	 */

	if (cqa->queued_alarms != NULL)
412
		return FALSE;
413

414
	debug (("Last Component. Removing CQA- Free=%d", free_object));
415
	if (free_object) {
416
		cqa->id = NULL;
417
		cqa->parent_client = NULL;
418
		e_cal_component_alarms_free (cqa->alarms);
419 420
		g_free (cqa);
	} else {
421
		e_cal_component_alarms_free (cqa->alarms);
422 423
		cqa->alarms = NULL;
	}
424 425

	return TRUE;
426 427
}

428 429 430
/**
 * has_known_notification:
 * Test for notification method and returns if it knows it or not.
431 432 433 434
 * @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.
435 436
 */
static gboolean
437 438
has_known_notification (ECalComponent *comp,
                        const gchar *alarm_uid)
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
{
	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) {
454 455 456 457 458 459 460
		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;
461
	}
462

463 464 465
	return FALSE;
}

466 467
/* Callback used when an alarm triggers */
static void
468 469 470
alarm_trigger_cb (gpointer alarm_id,
                  time_t trigger,
                  gpointer data)
471 472
{
	CompQueuedAlarms *cqa;
473
	ECalComponent *comp;
474
	QueuedAlarm *qa;
475 476
	ECalComponentAlarm *alarm;
	ECalComponentAlarmAction action;
477 478 479 480

	cqa = data;
	comp = cqa->alarms->comp;

481 482
	config_data_set_last_notification_time (
		cqa->parent_client->cal_client, trigger);
483
	debug (("Setting Last notification time to %s", e_ctime (&trigger)));
Federico Mena Quintero's avatar
Federico Mena Quintero committed
484

485
	qa = lookup_queued_alarm (cqa, alarm_id);
Rodrigo Moya's avatar
Rodrigo Moya committed
486 487
	if (!qa)
		return;
488 489 490 491 492 493 494

	/* 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.
	 */

495
	alarm = e_cal_component_get_alarm (comp, qa->instance->auid);
496 497
	if (!alarm)
		 return;
498

499 500
	e_cal_component_alarm_get_action (alarm, &action);
	e_cal_component_alarm_free (alarm);
501 502

	switch (action) {
503
	case E_CAL_COMPONENT_ALARM_AUDIO:
504
		audio_notification (trigger, cqa, alarm_id);
505 506
		break;

507
	case E_CAL_COMPONENT_ALARM_DISPLAY:
508 509 510
#ifdef HAVE_LIBNOTIFY
		popup_notification (trigger, cqa, alarm_id, TRUE);
#endif
511
		display_notification (trigger, cqa, alarm_id, TRUE);
512 513
		break;

514
	case E_CAL_COMPONENT_ALARM_EMAIL:
515
		mail_notification (trigger, cqa, alarm_id);
516 517
		break;

518
	case E_CAL_COMPONENT_ALARM_PROCEDURE:
519
		procedure_notification (trigger, cqa, alarm_id);
520 521 522
		break;

	default:
523
		g_return_if_reached ();
524
	}
525
	debug (("Notification sent: %d", action));
526 527
}

528
/* Adds the alarms in a ECalComponentAlarms structure to the alarms queued for a
529 530 531
 * particular client.  Also puts the triggers in the alarm timer queue.
 */
static void
532 533
add_component_alarms (ClientAlarms *ca,
                      ECalComponentAlarms *alarms)
534
{
535
	ECalComponentId *id;
536 537 538
	CompQueuedAlarms *cqa;
	GSList *l;

539
	/* No alarms? */
540
	if (alarms == NULL || alarms->alarms == NULL) {
541
		debug (("No alarms to add"));
542 543
		if (alarms)
			e_cal_component_alarms_free (alarms);
544 545 546
		return;
	}

547 548 549
	cqa = g_new (CompQueuedAlarms, 1);
	cqa->parent_client = ca;
	cqa->alarms = alarms;
Rodrigo Moya's avatar
Rodrigo Moya committed
550
	cqa->expecting_update = FALSE;
551 552

	cqa->queued_alarms = NULL;
553
	debug (("Creating CQA %p", cqa));
554

555
	for (l = alarms->alarms; l; l = l->next) {
556
		ECalComponentAlarmInstance *instance;
557 558
		gpointer alarm_id;
		QueuedAlarm *qa;
559

560 561
		instance = l->data;

562
		if (!has_known_notification (cqa->alarms->comp, instance->auid))
563 564
			continue;

565 566 567
		alarm_id = alarm_add (
			instance->trigger, alarm_trigger_cb, cqa, NULL);
		if (!alarm_id)
568 569
			continue;

570
		qa = g_new0 (QueuedAlarm, 1);
571 572
		qa->alarm_id = alarm_id;
		qa->instance = instance;
Chenthill Palanisamy's avatar
Chenthill Palanisamy committed
573
		qa->orig_trigger = instance->trigger;
574
		qa->snooze = FALSE;
575 576 577 578

		cqa->queued_alarms = g_slist_prepend (cqa->queued_alarms, qa);
	}

579
	id = e_cal_component_get_id (alarms->comp);
580 581 582

	/* If we failed to add all the alarms, then we should get rid of the cqa */
	if (cqa->queued_alarms == NULL) {
583
		e_cal_component_alarms_free (cqa->alarms);
584
		cqa->alarms = NULL;
585
		debug (("Failed to add all : %p", cqa));
586 587 588 589 590
		g_free (cqa);
		return;
	}

	cqa->queued_alarms = g_slist_reverse (cqa->queued_alarms);
591
	cqa->id = id;
592
	debug (("Alarm added for %s", id->uid));
593
	g_hash_table_insert (ca->uid_alarms_hash, cqa->id, cqa);
594 595
}

Federico Mena Quintero's avatar
Federico Mena Quintero committed
596
/* Loads the alarms of a client for a given range of time */
597
static void
598 599 600
load_alarms (ClientAlarms *ca,
             time_t start,
             time_t end)
601
{
602
	gchar *str_query, *iso_start, *iso_end;
603
	GError *error = NULL;
604

605
	debug (("..."));
606

Rodrigo Moya's avatar
Rodrigo Moya committed
607 608 609
	iso_start = isodate_from_time_t (start);
	if (!iso_start)
		return;
610

Rodrigo Moya's avatar
Rodrigo Moya committed
611 612
	iso_end = isodate_from_time_t (end);
	if (!iso_end) {
613
		g_free (iso_start);
Rodrigo Moya's avatar
Rodrigo Moya committed
614 615
		return;
	}
616

617 618 619
	str_query = g_strdup_printf (
		"(has-alarms-in-range? (make-time \"%s\") "
		"(make-time \"%s\"))", iso_start, iso_end);
Rodrigo Moya's avatar
Rodrigo Moya committed
620 621 622 623
	g_free (iso_start);
	g_free (iso_end);

	/* create the live query */
624
	if (ca->view) {
625
		debug (("Disconnecting old queries"));
626
		g_signal_handlers_disconnect_matched (
627 628 629
			ca->view, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, ca);
		g_object_unref (ca->view);
		ca->view = NULL;
630
	}
Rodrigo Moya's avatar
Rodrigo Moya committed
631

632 633 634 635 636 637 638 639
	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);
Rodrigo Moya's avatar
Rodrigo Moya committed
640
	} else {
641
		debug (("Setting Call backs"));
642

643
		g_signal_connect (
644 645
			ca->view, "objects-added",
			G_CALLBACK (query_objects_modified_cb), ca);
646
		g_signal_connect (
647 648
			ca->view, "objects-modified",
			G_CALLBACK (query_objects_modified_cb), ca);
649
		g_signal_connect (
650
			ca->view, "objects-removed",
651
			G_CALLBACK (query_objects_removed_cb), ca);
Rodrigo Moya's avatar
Rodrigo Moya committed
652

653
		e_cal_client_view_start (ca->view, &error);
654 655 656 657 658

		if (error != NULL) {
			g_warning (
				"%s: Failed to start view: %s",
				G_STRFUNC, error->message);
659 660
			g_error_free (error);
		}
Rodrigo Moya's avatar
Rodrigo Moya committed
661 662 663
	}

	g_free (str_query);
664 665
}

Federico Mena Quintero's avatar
Federico Mena Quintero committed
666 667 668 669
/* Loads today's remaining alarms for a client */
static void
load_alarms_for_today (ClientAlarms *ca)
{
670
	time_t now, from, day_end, day_start;
671
	icaltimezone *zone;
Federico Mena Quintero's avatar
Federico Mena Quintero committed
672 673

	now = time (NULL);
674 675
	zone = config_data_get_timezone ();
	day_start = time_day_begin_with_zone (now, zone);
676

Rodrigo Moya's avatar
Rodrigo Moya committed
677
	/* Make sure we don't miss some events from the last notification.
678
	 * We add 1 to the saved notification time to make the time ranges
Rodrigo Moya's avatar
Rodrigo Moya committed
679 680 681
	 * half-open; we do not want to display the "last" displayed alarm
	 * twice, once when it occurs and once when the alarm daemon restarts.
	 */
682
	from = config_data_get_last_notification_time (ca->cal_client) + 1;
683 684
	if (from <= 0)
		from = MAX (from, day_start);
685

686
	/* Add one hour after midnight, just to cover the delay in 30 minutes
687
	 * midnight checking. */
688
	day_end = time_day_end_with_zone (now, zone) + (60 * 60);
689
	debug (("From %s to %s", e_ctime (&from), e_ctime (&day_end)));
Rodrigo Moya's avatar
Rodrigo Moya committed
690
	load_alarms (ca, from, day_end);
Federico Mena Quintero's avatar
Federico Mena Quintero committed
691 692
}

693 694
/* Looks up a component's queued alarm structure in a client alarms structure */
static CompQueuedAlarms *
695 696
lookup_comp_queued_alarms (ClientAlarms *ca,
                           const ECalComponentId *id)
697
{
698
	return g_hash_table_lookup (ca->uid_alarms_hash, id);
699 700 701
}

static void
702 703
remove_alarms (CompQueuedAlarms *cqa,
               gboolean free_object)
704 705 706
{
	GSList *l;

707
	debug (("Removing for %p", cqa));
708 709 710

	tray_list_remove_cqa (cqa);

711 712 713 714 715 716
	for (l = cqa->queued_alarms; l;) {
		QueuedAlarm *qa;

		qa = l->data;

		/* Get the next element here because the list element will go
717 718
		 * away in remove_queued_alarm().  The qa will be freed there as
		 * well.
719 720 721 722
		 */
		l = l->next;

		alarm_remove (qa->alarm_id);
Rodrigo Moya's avatar
Rodrigo Moya committed
723
		remove_queued_alarm (cqa, qa->alarm_id, free_object, FALSE);
724
	}
725 726 727 728
}

/* Removes a component an its alarms */
static void
729 730
remove_comp (ClientAlarms *ca,
             ECalComponentId *id)
731 732 733
{
	CompQueuedAlarms *cqa;

734
	debug (("Removing uid %s", id->uid));
735 736 737 738 739

	if (id->rid && !(*(id->rid))) {
		g_free (id->rid);
		id->rid = NULL;
	}
740

741
	cqa = lookup_comp_queued_alarms (ca, id);
742
	if (!cqa)
743 744 745 746 747
		return;

	/* If a component is present, then it means we must have alarms queued
	 * for it.
	 */
748
	g_return_if_fail (cqa->queued_alarms != NULL);
749

750
	debug (("Removing CQA %p", cqa));
751
	remove_alarms (cqa, TRUE);
752 753 754 755 756
}

/* Called when a calendar component changes; we must reload its corresponding
 * alarms.
 */
757
struct _query_msg {
Matthew Barnes's avatar
Matthew Barnes committed
758
	Message header;
759
	GSList *objects;
760 761 762
	gpointer data;
};

763 764
static GSList *
duplicate_ical (const GSList *in_list)
765
{
766 767
	const GSList *l;
	GSList *out_list = NULL;
768
	for (l = in_list; l; l = l->next) {
769 770
		out_list = g_slist_prepend (
			out_list, icalcomponent_new_clone (l->data));
771
	}
772

773
	return g_slist_reverse (out_list);
774 775
}

776 777
static GSList *
duplicate_ecal (const GSList *in_list)
778
{
779 780
	const GSList *l;
	GSList *out_list = NULL;
781 782 783 784 785 786
	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);
787 788 789 790 791 792 793
		out_list = g_slist_prepend (out_list, id);
	}

	return g_slist_reverse (out_list);
}

static gboolean
794 795 796 797 798
get_alarms_for_object (ECalClient *cal_client,
                       const ECalComponentId *id,
                       time_t start,
                       time_t end,
                       ECalComponentAlarms **alarms)
799
{
Matthew Barnes's avatar
Matthew Barnes committed
800
	icalcomponent *icalcomp = NULL;
801 802 803 804 805 806 807 808 809
	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);

Matthew Barnes's avatar
Matthew Barnes committed
810 811
	e_cal_client_get_object_sync (
		cal_client, id->uid, id->rid, &icalcomp, NULL, NULL);
812

Matthew Barnes's avatar
Matthew Barnes committed
813
	if (icalcomp == NULL)
814 815 816 817 818 819 820
		return FALSE;

	comp = e_cal_component_new ();
	if (!e_cal_component_set_icalcomponent (comp, icalcomp)) {
		icalcomponent_free (icalcomp);
		g_object_unref (comp);
		return FALSE;
821
	}
822

823 824
	*alarms = e_cal_util_generate_alarms_for_comp (
		comp, start, end, omit, e_cal_client_resolve_tzid_cb,
825
		cal_client, config_data_get_timezone ());
826 827 828 829

	g_object_unref (comp);

	return TRUE;
830 831
}

832
static void
Matthew Barnes's avatar
Matthew Barnes committed
833
query_objects_changed_async (struct _query_msg *msg)
834 835
{
	ClientAlarms *ca;
836
	time_t from, day_end;
837
	ECalComponentAlarms *alarms;
838
	gboolean found;
839
	icaltimezone *zone;
840
	CompQueuedAlarms *cqa;
841 842
	GSList *l;
	GSList *objects;
843

Matthew Barnes's avatar
Matthew Barnes committed
844 845
	ca = msg->data;
	objects = msg->objects;
846

847
	from = config_data_get_last_notification_time (ca->cal_client);
848 849
	if (from == -1)
		from = time (NULL);
850 851
	else
		from += 1; /* we add 1 to make sure the alarm is not displayed twice */
852 853 854

	zone = config_data_get_timezone ();

Rodrigo Moya's avatar
Rodrigo Moya committed
855
	day_end = time_day_end_with_zone (time (NULL), zone);
856

857
	for (l = objects; l != NULL; l = l->next) {
858
		ECalComponentId *id;
859
		GSList *sl;
860 861
		ECalComponent *comp = e_cal_component_new ();

862
		e_cal_component_set_icalcomponent (comp, l->data);
863

864
		id = e_cal_component_get_id (comp);
865
		found = get_alarms_for_object (ca->cal_client, id, from, day_end, &alarms);
866

867
		if (!found) {
868
			debug (("No Alarm found for client %p", ca->cal_client));
Srinivasa Ragavan's avatar
Srinivasa Ragavan committed
869
			tray_list_remove_cqa (lookup_comp_queued_alarms (ca, id));
870
			remove_comp (ca, id);
871
			g_hash_table_remove (ca->uid_alarms_hash, id);
872 873 874
			e_cal_component_free_id (id);
			g_object_unref (comp);
			comp = NULL;
875
			continue;
876
		}
877

878
		cqa = lookup_comp_queued_alarms (ca, id);
879
		if (!cqa) {
880
			debug (("No currently queued alarms for %s", id->uid));
881
			add_component_alarms (ca, alarms);
882 883
			g_object_unref (comp);
			comp = NULL;
884 885
			continue;
		}
886

887
		debug (("Alarm Already Exist for %s", id->uid));
888 889
		/* If the alarms or the alarms list is empty,
		 * remove it after updating the cqa structure. */
890
		if (alarms == NULL || alarms->alarms == NULL) {
891

892 893
			/* Update the cqa and its queued alarms
			 * for changes in summary and alarm_uid.  */
894
			update_cqa (cqa, comp);
Chenthill Palanisamy's avatar
Chenthill Palanisamy committed
895

896 897 898 899 900 901 902 903 904
			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;
905

906 907 908 909 910
		/* add the new alarms */
		for (sl = cqa->alarms->alarms; sl; sl = sl->next) {
			ECalComponentAlarmInstance *instance;
			gpointer alarm_id;
			QueuedAlarm *qa;
Rodrigo Moya's avatar
Rodrigo Moya committed
911

912
			instance = sl->data;
Rodrigo Moya's avatar
Rodrigo Moya committed
913

914
			if (!has_known_notification (cqa->alarms->comp, instance->auid))
915 916
				continue;

917
			alarm_id = alarm_add (instance->trigger, alarm_trigger_cb, cqa, NULL);
918
			if (!alarm_id)
919
				continue;
Rodrigo Moya's avatar
Rodrigo Moya committed
920

921
			qa = g_new0 (QueuedAlarm, 1);
922 923 924
			qa->alarm_id = alarm_id;
			qa->instance = instance;
			qa->snooze = FALSE;
925
			qa->orig_trigger = instance->trigger;
926
			cqa->queued_alarms = g_slist_prepend (cqa->queued_alarms, qa);
927
			debug (("Adding %p to queue", qa));
928
		}
929

930
		cqa->queued_alarms = g_slist_reverse (cqa->queued_alarms);
931 932
		g_object_unref (comp);
		comp = NULL;
933
	}
934
	g_slist_free (objects);
Matthew Barnes's avatar
Matthew Barnes committed
935 936

	g_slice_free (struct _query_msg, msg);
937 938 939
}

static void
940 941 942
query_objects_modified_cb (ECalClientView *view,
                           const GSList *objects,
                           gpointer data)
943
{
Matthew Barnes's avatar
Matthew Barnes committed
944
	struct _query_msg *msg;
945

Matthew Barnes's avatar
Matthew Barnes committed
946 947 948 949
	msg = g_slice_new0 (struct _query_msg);
	msg->header.func = (MessageFunc) query_objects_changed_async;
	msg->objects = duplicate_ical (objects);
	msg->data = data;
950

Matthew Barnes's avatar
Matthew Barnes committed
951
	message_push ((Message *) msg);
952 953 954 955 956 957
}

/* Called when a calendar component is removed; we must delete its corresponding
 * alarms.
 */
static void
Matthew Barnes's avatar
Matthew Barnes committed
958
query_objects_removed_async (struct _query_msg *msg)
959 960
{
	ClientAlarms *ca;
961 962
	GSList *l;
	GSList *objects;
963

Matthew Barnes's avatar
Matthew Barnes committed
964 965
	ca = msg->data;
	objects = msg->objects;
966

967
	debug (("Removing %d objects", g_slist_length (objects)));
968 969

	for (l = objects; l != NULL; l = l->next) {
970 971
		/* If the alarm is already triggered remove it. */
		tray_list_remove_cqa (lookup_comp_queued_alarms (ca, l->data));
972
		remove_comp (ca, l->data);
973
		g_hash_table_remove (ca->uid_alarms_hash, l->data);
974 975 976
		e_cal_component_free_id (l->data);
	}

977
	g_slist_free (objects);
Matthew Barnes's avatar
Matthew Barnes committed
978 979

	g_slice_free (struct _query_msg, msg);
980 981
}

982
static void
983 984 985
query_objects_removed_cb (ECalClientView *view,
                          const GSList *uids,
                          gpointer data)
986
{
Matthew Barnes's avatar
Matthew Barnes committed
987
	struct _query_msg *msg;
988

Matthew Barnes's avatar
Matthew Barnes committed
989 990
	msg = g_slice_new0 (struct _query_msg);
	msg->header.func = (MessageFunc) query_objects_removed_async;
991
	msg->objects = duplicate_ecal (uids);
Matthew Barnes's avatar
Matthew Barnes committed
992
	msg->data = data;
993

Matthew Barnes's avatar
Matthew Barnes committed
994
	message_push ((Message *) msg);
995
}
996

997 998
/* Notification functions */

999 1000 1001 1002
/* Creates a snooze alarm based on an existing one.  The snooze offset is
 * compued with respect to the current time.
 */
static void
1003 1004 1005
create_snooze (CompQueuedAlarms *cqa,
               gpointer alarm_id,
               gint snooze_mins)
1006
{
Rodrigo Moya's avatar
Rodrigo Moya committed
1007
	QueuedAlarm *orig_qa;
1008 1009 1010 1011
	time_t t;
	gpointer new_id;

	orig_qa = lookup_queued_alarm (cqa, alarm_id);
Rodrigo Moya's avatar
Rodrigo Moya committed
1012 1013
	if (!orig_qa)
		return;
1014 1015 1016 1017 1018 1019

	t = time (NULL);
	t += snooze_mins * 60;

	new_id = alarm_add (t, alarm_trigger_cb, cqa, NULL);
	if (!new_id) {
1020
		debug (("Unable to schedule trigger for %s", e_ctime (&t)));
1021 1022 1023
		return;
	}

Rodrigo Moya's avatar
Rodrigo Moya committed
1024 1025 1026
	orig_qa->instance->trigger = t;
	orig_qa->alarm_id = new_id;
	orig_qa->snooze = TRUE;
1027 1028 1029 1030 1031 1032 1033
	#ifdef HAVE_LIBNOTIFY
	if (orig_qa->notify) {
		notify_notification_close (orig_qa->notify, NULL);
		g_clear_object (&orig_qa->notify);
	}
	#endif
	debug (("Adding an alarm at %s", e_ctime (&t)));
1034 1035
}

Federico Mena Quintero's avatar
Federico Mena Quintero committed
1036 1037
/* Launches a component editor for a component */
static void
1038 1039
edit_component (ECalClient *cal_client,
                ECalComponent *comp)
Federico Mena Quintero's avatar
Federico Mena Quintero committed
1040
{
1041 1042 1043 1044 1045 1046
	ESource *source;
	gchar *command_line;
	const gchar *scheme;
	const gchar *comp_uid;
	const gchar *source_uid;
	GError *error = NULL;
Federico Mena Quintero's avatar
Federico Mena Quintero committed
1047

1048 1049
	/* XXX Don't we have a function to construct these URIs?
	 *     How are other apps expected to know this stuff? */
1050

1051
	source = e_client_get_source (E_CLIENT (cal_client));
1052
	source_uid = e_source_get_uid (source);
Federico Mena Quintero's avatar
Federico Mena Quintero committed
1053

1054
	e_cal_component_get_uid (comp, &comp_uid);
Federico Mena Quintero's avatar
Federico Mena Quintero committed
1055

1056 1057
	switch (e_cal_client_get_source_type (cal_client)) {
		case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
1058 1059
			scheme = "calendar:";
			break;
1060
		case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
1061 1062
			scheme = "task:";
			break;
1063
		case E_CAL_CLIENT_SOURCE_TYPE_MEMOS:
1064 1065 1066 1067
			scheme = "memo:";
			break;
		default:
			g_return_if_reached ();
JP Rosevear's avatar
JP Rosevear committed
1068
	}
1069

1070 1071 1072
	command_line = g_strdup_printf (
		"%s %s///?source-uid=%s&comp-uid=%s",
		PACKAGE, scheme, source_uid, comp_uid);
Federico Mena Quintero's avatar
Federico Mena Quintero committed
1073

1074 1075 1076 1077
	if (!g_spawn_command_line_async (command_line, &error)) {
		g_critical ("%s", error->message);
		g_error_free (error);
	}
Federico Mena Quintero's avatar
Federico Mena Quintero committed
1078

1079
	g_free (command_line);
Federico Mena Quintero's avatar
Federico Mena Quintero committed
1080 1081
}

1082 1083
static void
print_component (ECalClient *cal_client,
1084
                 ECalComponent *comp)
1085
{
1086 1087 1088 1089 1090 1091
	print_comp (
		comp,
		cal_client,
		config_data_get_timezone (),
		config_data_get_24_hour_format (),
		GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG);
1092 1093
}

1094
typedef struct {
1095 1096 1097
	gchar *summary;
	gchar *description;
	gchar *location;
1098
	gboolean blink_state;
1099
	gboolean snooze_set;
1100 1101
	gint blink_id;
	time_t trigger;
1102 1103
	CompQueuedAlarms *cqa;
	gpointer alarm_id;
1104
	ECalComponent *comp;
1105 1106
	ECalClient *cal_client;
	ECalClientView *view;
1107
	GdkPixbuf *image;
1108
	GtkTreeIter iter;
1109
	gboolean is_in_tree;
1110
} TrayIconData;
1111

1112 1113 1114 1115 1116
static void
free_tray_icon_data (TrayIconData *tray_data)
{
	g_return_if_fail (tray_data != NULL);

1117
	if (tray_data->summary) {
1118 1119 1120 1121
		g_free (tray_data->summary);
		tray_data->summary = NULL;
	}

1122
	if (tray_data->description) {
1123 1124 1125 1126
		g_free (tray_data->description);
		tray_data->description = NULL;
	}

1127
	if (tray_data->location) {
1128
		g_free (tray_data->location);
1129 1130 1131
		tray_data->location = NULL;
	}

1132 1133
	g_object_unref (tray_data->cal_client);
	tray_data->cal_client = NULL;
1134

1135 1136 1137
	g_signal_handlers_disconnect_matched (
		tray_data->view, G_SIGNAL_MATCH_FUNC,
		0, 0, NULL, on_dialog_objs_removed_cb, NULL);
1138 1139
	g_object_unref (tray_data->view);
	tray_data->view = NULL;
1140 1141 1142

	g_object_unref (tray_data->comp);
	tray_data->comp = NULL;
1143

1144 1145 1146 1147 1148 1149 1150
	tray_data->cqa = NULL;
	tray_data->alarm_id = NULL;
	tray_data->image = NULL;

	g_free (tray_data);
}

1151
static void
Matthew Barnes's avatar
Matthew Barnes committed
1152
on_dialog_objs_removed_async (struct _query_msg *msg)
1153
{
1154
	TrayIconData *tray_data;
1155 1156
	GSList *l, *objects;
	ECalComponentId *our_id;
1157

1158
	debug (("..."));
1159

Matthew Barnes's avatar
Matthew Barnes committed
1160 1161
	tray_data = msg->data