aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCharles Kerr <charles.kerr@canonical.com>2013-10-17 03:39:12 +0000
committerTarmac <Unknown>2013-10-17 03:39:12 +0000
commite38b6293bb37557d27efd052c82ee44d70996077 (patch)
treeffcb823fe27b30a448422a7ef72f3727a81d4469
parent3cb74c566f21cead559308c94f3b3995e35e6fa2 (diff)
parent05c1039f4b9f0014371bb767089755317ea2d322 (diff)
downloadayatana-indicator-datetime-e38b6293bb37557d27efd052c82ee44d70996077.tar.gz
ayatana-indicator-datetime-e38b6293bb37557d27efd052c82ee44d70996077.tar.bz2
ayatana-indicator-datetime-e38b6293bb37557d27efd052c82ee44d70996077.zip
== Changes to planner-eds:
The get-appointments GTask has a new task subtype for pulling an ECalComponent's uris asynchronously. When get_appointments() is called, create one GTask. We add subtasks to it for each client we know of for calling e_cal_client_generate_instances(). What's new is that for each ECalComponent we find in generate_instances(), we add another new subtask that tries to get the uris for that component. == Testing changes: Make "planner" a property in IndicatorDatetimeService so that we can swap in different appointment planners at runtime. This is for unit testing purposes. Add a mechanism for testing snap decisions without an EDS backend. == Service changes: Every time the appointment list changes, walk through it to find the alarm that will occur the soonest. Set a timer to wake up at that time. When the timer is reached, pop up a snap decision for each alarm set to that time. If the user clicks "OK", dispatch the URL associated with that alarm. Made the appointment menuitems clickable, they now dispatch the appointment's URL. Fixes: https://bugs.launchpad.net/bugs/1233176. Approved by PS Jenkins bot, Ted Gould.
-rw-r--r--README8
-rw-r--r--configure.ac2
-rw-r--r--debian/control1
-rw-r--r--src/Makefile.am2
-rw-r--r--src/main.c49
-rw-r--r--src/planner-eds.c219
-rw-r--r--src/planner-eds.h2
-rw-r--r--src/planner-mock.c178
-rw-r--r--src/planner-mock.h58
-rw-r--r--src/planner.c20
-rw-r--r--src/planner.h6
-rw-r--r--src/service.c386
-rw-r--r--src/service.h7
13 files changed, 846 insertions, 92 deletions
diff --git a/README b/README
index 5dc9764..b31db05 100644
--- a/README
+++ b/README
@@ -7,11 +7,17 @@ ACTIONS
Parameter: None
* "activate-planner"
- Description: opens up a calendar appointment editor.
+ Description: opens an appointment editor.
State: None
Parameter: int64, a time_t hinting which day/time to show in the planner,
or 0 for the current day
+ * "activate-appointment"
+ Description: opens an appointment editor to the specified appointment.
+ State: None
+ Parameter: string, an opaque uid to specify which appointment to use.
+ This uid comes from the menuitems' target values.
+
* "set-location"
Description: Set the current location. This will try to set the current
timezone to the new location's timezone.
diff --git a/configure.ac b/configure.ac
index c41dc38..040f222 100644
--- a/configure.ac
+++ b/configure.ac
@@ -51,6 +51,7 @@ GEOCLUE_REQUIRED_VERSION=0.12.0
ICAL_REQUIRED_VERSION=0.48
ECAL_REQUIRED_VERSION=3.5
EDS_REQUIRED_VERSION=3.5
+LIBNOTIFY_REQUIRED_VERSION=0.7.6
URL_DISPATCHER_1_REQUIRED_VERSION=1
JSON_GLIB_REQUIRED_VERSION=0.16.2
@@ -62,6 +63,7 @@ PKG_CHECK_MODULES(SERVICE, [glib-2.0 >= $GLIB_REQUIRED_VERSION
libical >= $ICAL_REQUIRED_VERSION
libecal-1.2 >= $ECAL_REQUIRED_VERSION
libedataserver-1.2 >= $EDS_REQUIRED_VERSION
+ libnotify >= $LIBNOTIFY_REQUIRED_VERSION
url-dispatcher-1 >= $URL_DISPATCHER_1_REQUIRED_VERSION
json-glib-1.0 >= $JSON_GLIB_REQUIRED_VERSION])
diff --git a/debian/control b/debian/control
index 22828b5..548f7b0 100644
--- a/debian/control
+++ b/debian/control
@@ -10,6 +10,7 @@ Build-Depends: debhelper (>= 9),
libxorg-gtest-dev,
libgtest-dev,
libglib2.0-dev (>= 2.35.4),
+ libnotify-dev (>= 0.7.6),
libido3-0.1-dev (>= 0.2.90),
libgeoclue-dev (>= 0.12.0),
libecal1.2-dev (>= 3.5),
diff --git a/src/Makefile.am b/src/Makefile.am
index 093a258..640650a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -20,6 +20,8 @@ libindicator_datetime_service_a_CFLAGS = \
libindicator_datetime_service_a_SOURCES = \
planner.c \
planner.h \
+ planner-mock.c \
+ planner-mock.h \
planner-eds.c \
planner-eds.h \
service.c \
diff --git a/src/main.c b/src/main.c
index c75b2d7..f791683 100644
--- a/src/main.c
+++ b/src/main.c
@@ -24,40 +24,81 @@
#include <glib/gi18n.h>
#include <gio/gio.h>
+#include <libnotify/notify.h>
+#include "planner-eds.h"
+#include "planner-mock.h"
#include "service.h"
/***
****
***/
+/* When enabled, new alarms will show up every minute to test snap decisions */
+static gboolean test_alarms = FALSE;
+
+static GOptionEntry entries[] = {
+ { "test-alarms", '\0', 0, G_OPTION_ARG_NONE, &test_alarms, "Test Alarms", NULL },
+ { NULL }
+};
+
static void
on_name_lost (gpointer instance G_GNUC_UNUSED, gpointer loop)
{
g_message ("exiting: service couldn't acquire or lost ownership of busname");
- g_main_loop_quit ((GMainLoop*)loop);
+
+ if (!test_alarms)
+ g_main_loop_quit ((GMainLoop*)loop);
}
int
main (int argc G_GNUC_UNUSED, char ** argv G_GNUC_UNUSED)
{
- GMainLoop * loop;
+ GOptionContext * context;
+ GError * error;
+ IndicatorDatetimePlanner * planner;
IndicatorDatetimeService * service;
+ GMainLoop * loop;
/* boilerplate i18n */
setlocale (LC_ALL, "");
bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
textdomain (GETTEXT_PACKAGE);
+ /* init libnotify */
+ if (!notify_init ("indicator-datetime-service"))
+ g_critical ("libnotify initialization failed");
+
+ /* parse command-line options */
+ context = g_option_context_new (NULL);
+ g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE);
+ if (!g_option_context_parse (context, &argc, &argv, &error))
+ {
+ g_print("option parsing failed: %s\n", error->message);
+ return EXIT_FAILURE;
+ }
+
+ /* set up the planner */
+ if (test_alarms)
+ {
+ g_message ("Using fake appointment book for testing alarms.");
+ planner = indicator_datetime_planner_mock_new ();
+ }
+ else
+ {
+ planner = indicator_datetime_planner_eds_new ();
+ }
+
/* run */
- service = indicator_datetime_service_new ();
+ service = indicator_datetime_service_new (planner);
loop = g_main_loop_new (NULL, FALSE);
g_signal_connect (service, INDICATOR_DATETIME_SERVICE_SIGNAL_NAME_LOST,
G_CALLBACK(on_name_lost), loop);
g_main_loop_run (loop);
/* cleanup */
- g_clear_object (&service);
g_main_loop_unref (loop);
+ g_object_unref (service);
+ g_object_unref (planner);
return 0;
}
diff --git a/src/planner-eds.c b/src/planner-eds.c
index f121a32..876fdfc 100644
--- a/src/planner-eds.c
+++ b/src/planner-eds.c
@@ -43,78 +43,180 @@ G_DEFINE_QUARK ("source-client", source_client)
/***
****
+**** my_get_appointments() helpers
+****
***/
-void
-indicator_datetime_appt_free (struct IndicatorDatetimeAppt * appt)
+/* whole-task data that all the subtasks can see */
+struct appointment_task_data
{
- if (appt != NULL)
- {
- g_date_time_unref (appt->end);
- g_date_time_unref (appt->begin);
- g_free (appt->color);
- g_free (appt->summary);
- g_slice_free (struct IndicatorDatetimeAppt, appt);
- }
-}
-
-/***
-**** my_get_appointments() helpers
-***/
+ /* a ref to the planner's cancellable */
+ GCancellable * cancellable;
-struct get_appointments_task_data
-{
/* how many subtasks are still running on */
int subtask_count;
/* the list of appointments to be returned */
GSList * appointments;
-
- /* ensure that recurring events don't get multiple IndicatorDatetimeAppts */
- GHashTable * added;
};
+static struct appointment_task_data *
+appointment_task_data_new (GCancellable * cancellable)
+{
+ struct appointment_task_data * data;
+
+ data = g_slice_new0 (struct appointment_task_data);
+ data->cancellable = g_object_ref (cancellable);
+ return data;
+}
+
static void
-get_appointments_task_data_free (gpointer gdata)
+appointment_task_data_free (gpointer gdata)
{
- struct get_appointments_task_data * data = gdata;
- g_hash_table_unref (data->added);
- g_slice_free (struct get_appointments_task_data, data);
+ struct appointment_task_data * data = gdata;
+
+ g_object_unref (data->cancellable);
+
+ g_slice_free (struct appointment_task_data, data);
}
static void
-on_all_subtasks_done (GTask * task)
+appointment_task_done (GTask * task)
{
- struct get_appointments_task_data * data = g_task_get_task_data (task);
+ struct appointment_task_data * data = g_task_get_task_data (task);
+
g_task_return_pointer (task, data->appointments, NULL);
g_object_unref (task);
}
-struct get_appointments_subtask_data
+static void
+appointment_task_decrement_subtasks (GTask * task)
{
+ struct appointment_task_data * data = g_task_get_task_data (task);
+
+ if (g_atomic_int_dec_and_test (&data->subtask_count))
+ appointment_task_done (task);
+}
+
+static void
+appointment_task_increment_subtasks (GTask * task)
+{
+ struct appointment_task_data * data = g_task_get_task_data (task);
+
+ g_atomic_int_inc (&data->subtask_count);
+}
+
+/**
+*** get-the-appointment's-uri subtasks
+**/
+
+struct appointment_uri_subtask_data
+{
+ /* The parent task */
GTask * task;
- gchar * color;
+ /* The appointment whose uri we're looking for.
+ This pointer is owned by the Task and isn't reffed/unreffed by the subtask */
+ struct IndicatorDatetimeAppt * appt;
};
static void
-on_subtask_done (gpointer gsubdata)
+appointment_uri_subtask_done (struct appointment_uri_subtask_data * subdata)
+{
+ GTask * task = subdata->task;
+
+ /* free the subtask data */
+ g_slice_free (struct appointment_uri_subtask_data, subdata);
+
+ appointment_task_decrement_subtasks (task);
+}
+
+static struct appointment_uri_subtask_data *
+appointment_uri_subtask_data_new (GTask * task, struct IndicatorDatetimeAppt * appt)
+{
+ struct appointment_uri_subtask_data * subdata;
+
+ appointment_task_increment_subtasks (task);
+
+ subdata = g_slice_new0 (struct appointment_uri_subtask_data);
+ subdata->task = task;
+ subdata->appt = appt;
+ return subdata;
+}
+
+static void
+on_appointment_uris_ready (GObject * client,
+ GAsyncResult * res,
+ gpointer gsubdata)
{
- struct get_appointments_subtask_data * subdata;
+ GSList * uris;
+ GError * error;
+ struct appointment_uri_subtask_data * subdata = gsubdata;
+
+ uris = NULL;
+ error = NULL;
+ e_cal_client_get_attachment_uris_finish (E_CAL_CLIENT(client), res, &uris, &error);
+ if (error != NULL)
+ {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Error getting appointment uris: %s", error->message);
+
+ g_error_free (error);
+ }
+ else if (uris != NULL)
+ {
+ struct IndicatorDatetimeAppt * appt = subdata->appt;
+ appt->url = g_strdup (uris->data); /* copy the first URL */
+ g_debug ("found url '%s' for appointment '%s'", appt->url, appt->summary);
+ e_client_util_free_string_slist (uris);
+ }
+
+ appointment_uri_subtask_done (subdata);
+}
+
+/**
+*** enumerate-the-components subtasks
+**/
+
+/* data struct for the enumerate-components subtask */
+struct appointment_component_subtask_data
+{
+ /* The parent task */
GTask * task;
- struct get_appointments_task_data * data;
- subdata = gsubdata;
- task = subdata->task;
+ /* The client we're walking through. The subtask owns a ref to this */
+ ECalClient * client;
+
+ /* The appointment's color coding. The subtask owns this string */
+ gchar * color;
+};
+
+static void
+on_appointment_component_subtask_done (gpointer gsubdata)
+{
+ struct appointment_component_subtask_data * subdata = gsubdata;
+ GTask * task = subdata->task;
/* free the subtask data */
g_free (subdata->color);
- g_slice_free (struct get_appointments_subtask_data, subdata);
+ g_object_unref (subdata->client);
+ g_slice_free (struct appointment_component_subtask_data, subdata);
- /* poke the task */
- data = g_task_get_task_data (task);
- if (g_atomic_int_dec_and_test (&data->subtask_count))
- on_all_subtasks_done (task);
+ appointment_task_decrement_subtasks (task);
+}
+
+static struct appointment_component_subtask_data *
+appointment_component_subtask_data_new (GTask * task, ECalClient * client, const gchar * color)
+{
+ struct appointment_component_subtask_data * subdata;
+
+ appointment_task_increment_subtasks (task);
+
+ subdata = g_slice_new0 (struct appointment_component_subtask_data);
+ subdata->task = task;
+ subdata->client = g_object_ref (client);
+ subdata->color = g_strdup (color);
+ return subdata;
}
static gboolean
@@ -124,8 +226,8 @@ my_get_appointments_foreach (ECalComponent * component,
gpointer gsubdata)
{
const ECalComponentVType vtype = e_cal_component_get_vtype (component);
- struct get_appointments_subtask_data * subdata = gsubdata;
- struct get_appointments_task_data * data = g_task_get_task_data (subdata->task);
+ struct appointment_component_subtask_data * subdata = gsubdata;
+ struct appointment_task_data * data = g_task_get_task_data (subdata->task);
if ((vtype == E_CAL_COMPONENT_EVENT) || (vtype == E_CAL_COMPONENT_TODO))
{
@@ -136,7 +238,6 @@ my_get_appointments_foreach (ECalComponent * component,
e_cal_component_get_status (component, &status);
if ((uid != NULL) &&
- (!g_hash_table_contains (data->added, uid)) &&
(status != ICAL_STATUS_COMPLETED) &&
(status != ICAL_STATUS_CANCELLED))
{
@@ -145,6 +246,7 @@ my_get_appointments_foreach (ECalComponent * component,
GSList * recur_list;
ECalComponentText text;
struct IndicatorDatetimeAppt * appt;
+ struct appointment_uri_subtask_data * uri_subdata;
appt = g_slice_new0 (struct IndicatorDatetimeAppt);
@@ -169,13 +271,22 @@ my_get_appointments_foreach (ECalComponent * component,
appt->color = g_strdup (subdata->color);
appt->is_event = vtype == E_CAL_COMPONENT_EVENT;
appt->summary = g_strdup (text.value);
+ appt->uid = g_strdup (uid);
alarm_uids = e_cal_component_get_alarm_uids (component);
appt->has_alarms = alarm_uids != NULL;
cal_obj_uid_list_free (alarm_uids);
data->appointments = g_slist_prepend (data->appointments, appt);
- g_hash_table_add (data->added, g_strdup(uid));
+
+ /* start a new subtask to get the associated URIs */
+ uri_subdata = appointment_uri_subtask_data_new (subdata->task, appt);
+ e_cal_client_get_attachment_uris (subdata->client,
+ uid,
+ NULL,
+ data->cancellable,
+ on_appointment_uris_ready,
+ uri_subdata);
}
}
@@ -197,7 +308,6 @@ my_get_appointments (IndicatorDatetimePlanner * planner,
priv_t * p;
const char * str;
icaltimezone * default_timezone;
- struct get_appointments_task_data * data;
const int64_t begin = g_date_time_to_unix (begin_datetime);
const int64_t end = g_date_time_to_unix (end_datetime);
GTask * task;
@@ -223,17 +333,18 @@ my_get_appointments (IndicatorDatetimePlanner * planner,
*** walk through the sources to build the appointment list
**/
- data = g_slice_new0 (struct get_appointments_task_data);
- data->added = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
task = g_task_new (planner, p->cancellable, callback, user_data);
- g_task_set_task_data (task, data, get_appointments_task_data_free);
+ g_task_set_task_data (task,
+ appointment_task_data_new (p->cancellable),
+ appointment_task_data_free);
subtasks_added = FALSE;
for (l=p->sources; l!=NULL; l=l->next)
{
ESource * source;
ECalClient * client;
- struct get_appointments_subtask_data * subdata;
+ const char * color;
+ struct appointment_component_subtask_data * subdata;
source = l->data;
client = g_object_get_qdata (l->data, source_client_quark());
@@ -243,11 +354,9 @@ my_get_appointments (IndicatorDatetimePlanner * planner,
if (default_timezone != NULL)
e_cal_client_set_default_timezone (client, default_timezone);
- subdata = g_slice_new (struct get_appointments_subtask_data);
- subdata->task = task;
- subdata->color = e_source_selectable_dup_color (e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR));
-
- g_atomic_int_inc (&data->subtask_count);
+ /* start a new subtask to enumerate all the components in this client. */
+ color = e_source_selectable_get_color (e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR));
+ subdata = appointment_component_subtask_data_new (task, client, color);
subtasks_added = TRUE;
e_cal_client_generate_instances (client,
begin,
@@ -255,11 +364,11 @@ my_get_appointments (IndicatorDatetimePlanner * planner,
p->cancellable,
my_get_appointments_foreach,
subdata,
- on_subtask_done);
+ on_appointment_component_subtask_done);
}
if (!subtasks_added)
- on_all_subtasks_done (task);
+ appointment_task_done (task);
}
static GSList *
@@ -270,7 +379,7 @@ my_get_appointments_finish (IndicatorDatetimePlanner * self G_GNUC_UNUSED,
return g_task_propagate_pointer (G_TASK(res), error);
}
-gboolean
+static gboolean
my_is_configured (IndicatorDatetimePlanner * planner)
{
IndicatorDatetimePlannerEds * self;
diff --git a/src/planner-eds.h b/src/planner-eds.h
index a2c803a..dea9371 100644
--- a/src/planner-eds.h
+++ b/src/planner-eds.h
@@ -51,8 +51,6 @@ struct _IndicatorDatetimePlannerEdsClass
IndicatorDatetimePlannerClass parent_class;
};
-gboolean indicator_datetime_planner_eds_is_usable (void);
-
IndicatorDatetimePlanner * indicator_datetime_planner_eds_new (void);
G_END_DECLS
diff --git a/src/planner-mock.c b/src/planner-mock.c
new file mode 100644
index 0000000..e67ad7e
--- /dev/null
+++ b/src/planner-mock.c
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3, as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranties of
+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "planner-mock.h"
+
+struct _IndicatorDatetimePlannerMockPriv
+{
+ gboolean is_configured;
+};
+
+typedef IndicatorDatetimePlannerMockPriv priv_t;
+
+G_DEFINE_TYPE (IndicatorDatetimePlannerMock,
+ indicator_datetime_planner_mock,
+ INDICATOR_TYPE_DATETIME_PLANNER)
+
+/***
+**** IndicatorDatetimePlanner virtual funcs
+***/
+
+static void
+my_get_appointments (IndicatorDatetimePlanner * planner,
+ GDateTime * begin_datetime,
+ GDateTime * end_datetime G_GNUC_UNUSED,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GTask * task;
+ GSList * appointments;
+ struct IndicatorDatetimeAppt * appt;
+ struct IndicatorDatetimeAppt * prev;
+
+ task = g_task_new (planner, NULL, callback, user_data);
+
+ /**
+ *** Build the appointments list
+ **/
+
+ appointments = NULL;
+
+ /* add a daily appointment that occurs at the beginning of the next minute */
+ appt = g_slice_new0 (struct IndicatorDatetimeAppt);
+ appt->is_daily = TRUE;
+ appt->begin = g_date_time_add_seconds (begin_datetime, 60-g_date_time_get_seconds(begin_datetime));
+ appt->end = g_date_time_add_minutes (appt->begin, 1);
+ appt->color = g_strdup ("#00FF00");
+ appt->is_event = TRUE;
+ appt->summary = g_strdup ("Daily alarm");
+ appt->uid = g_strdup ("this uid isn't very random.");
+ appt->has_alarms = TRUE;
+ appt->url = g_strdup ("alarm:///some-alarm-info-goes-here");
+ appointments = g_slist_prepend (appointments, appt);
+ prev = appt;
+
+ /* and add one for a minute later that has an alarm uri */
+ appt = g_slice_new0 (struct IndicatorDatetimeAppt);
+ appt->is_daily = TRUE;
+ appt->begin = g_date_time_add_minutes (prev->end, 1);
+ appt->end = g_date_time_add_minutes (appt->begin, 1);
+ appt->color = g_strdup ("#0000FF");
+ appt->is_event = TRUE;
+ appt->summary = g_strdup ("Second Daily alarm");
+ appt->uid = g_strdup ("this uid isn't very random either.");
+ appt->has_alarms = FALSE;
+ appointments = g_slist_prepend (appointments, appt);
+
+ /* done */
+ g_task_return_pointer (task, appointments, NULL);
+ g_object_unref (task);
+}
+
+static GSList *
+my_get_appointments_finish (IndicatorDatetimePlanner * self G_GNUC_UNUSED,
+ GAsyncResult * res,
+ GError ** error)
+{
+ return g_task_propagate_pointer (G_TASK(res), error);
+}
+
+static gboolean
+my_is_configured (IndicatorDatetimePlanner * planner)
+{
+ IndicatorDatetimePlannerMock * self;
+ self = INDICATOR_DATETIME_PLANNER_MOCK (planner);
+ return self->priv->is_configured;
+}
+
+static void
+my_activate (IndicatorDatetimePlanner * self G_GNUC_UNUSED)
+{
+ g_message ("%s %s", G_STRLOC, G_STRFUNC);
+}
+
+static void
+my_activate_time (IndicatorDatetimePlanner * self G_GNUC_UNUSED,
+ GDateTime * activate_time)
+{
+ gchar * str = g_date_time_format (activate_time, "%F %T");
+ g_message ("%s %s: %s", G_STRLOC, G_STRFUNC, str);
+ g_free (str);
+}
+
+/***
+**** GObject virtual funcs
+***/
+
+static void
+my_dispose (GObject * o)
+{
+ G_OBJECT_CLASS (indicator_datetime_planner_mock_parent_class)->dispose (o);
+}
+
+/***
+**** Instantiation
+***/
+
+static void
+indicator_datetime_planner_mock_class_init (IndicatorDatetimePlannerMockClass * klass)
+{
+ GObjectClass * object_class;
+ IndicatorDatetimePlannerClass * planner_class;
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = my_dispose;
+
+ planner_class = INDICATOR_DATETIME_PLANNER_CLASS (klass);
+ planner_class->is_configured = my_is_configured;
+ planner_class->activate = my_activate;
+ planner_class->activate_time = my_activate_time;
+ planner_class->get_appointments = my_get_appointments;
+ planner_class->get_appointments_finish = my_get_appointments_finish;
+
+ g_type_class_add_private (klass, sizeof (IndicatorDatetimePlannerMockPriv));
+}
+
+static void
+indicator_datetime_planner_mock_init (IndicatorDatetimePlannerMock * self)
+{
+ priv_t * p;
+
+ p = G_TYPE_INSTANCE_GET_PRIVATE (self,
+ INDICATOR_TYPE_DATETIME_PLANNER_MOCK,
+ IndicatorDatetimePlannerMockPriv);
+
+ p->is_configured = TRUE;
+
+ self->priv = p;
+}
+
+/***
+**** Public
+***/
+
+IndicatorDatetimePlanner *
+indicator_datetime_planner_mock_new (void)
+{
+ gpointer o = g_object_new (INDICATOR_TYPE_DATETIME_PLANNER_MOCK, NULL);
+
+ return INDICATOR_DATETIME_PLANNER (o);
+}
diff --git a/src/planner-mock.h b/src/planner-mock.h
new file mode 100644
index 0000000..8d7d7c2
--- /dev/null
+++ b/src/planner-mock.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3, as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranties of
+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __INDICATOR_DATETIME_PLANNER_MOCK__H__
+#define __INDICATOR_DATETIME_PLANNER_MOCK__H__
+
+#include "planner.h" /* parent class */
+
+G_BEGIN_DECLS
+
+#define INDICATOR_TYPE_DATETIME_PLANNER_MOCK (indicator_datetime_planner_mock_get_type())
+#define INDICATOR_DATETIME_PLANNER_MOCK(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), INDICATOR_TYPE_DATETIME_PLANNER_MOCK, IndicatorDatetimePlannerMock))
+#define INDICATOR_DATETIME_PLANNER_MOCK_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), INDICATOR_TYPE_DATETIME_PLANNER_MOCK, IndicatorDatetimePlannerMockClass))
+#define INDICATOR_IS_DATETIME_PLANNER_MOCK(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), INDICATOR_TYPE_DATETIME_PLANNER_MOCK))
+
+typedef struct _IndicatorDatetimePlannerMock IndicatorDatetimePlannerMock;
+typedef struct _IndicatorDatetimePlannerMockPriv IndicatorDatetimePlannerMockPriv;
+typedef struct _IndicatorDatetimePlannerMockClass IndicatorDatetimePlannerMockClass;
+
+GType indicator_datetime_planner_mock_get_type (void);
+
+/**
+ * An IndicatorDatetimePlanner which uses Evolution Data Server
+ * to get its list of appointments.
+ */
+struct _IndicatorDatetimePlannerMock
+{
+ /*< private >*/
+ IndicatorDatetimePlanner parent;
+ IndicatorDatetimePlannerMockPriv * priv;
+};
+
+struct _IndicatorDatetimePlannerMockClass
+{
+ IndicatorDatetimePlannerClass parent_class;
+};
+
+IndicatorDatetimePlanner * indicator_datetime_planner_mock_new (void);
+
+G_END_DECLS
+
+#endif /* __INDICATOR_DATETIME_PLANNER_MOCK__H__ */
diff --git a/src/planner.c b/src/planner.c
index e826c2c..9b9a77f 100644
--- a/src/planner.c
+++ b/src/planner.c
@@ -259,3 +259,23 @@ indicator_datetime_planner_get_timezone (IndicatorDatetimePlanner * self)
return self->priv->timezone;
}
+
+/***
+****
+***/
+
+void
+indicator_datetime_appt_free (struct IndicatorDatetimeAppt * appt)
+{
+ if (appt != NULL)
+ {
+ g_date_time_unref (appt->end);
+ g_date_time_unref (appt->begin);
+ g_free (appt->color);
+ g_free (appt->summary);
+ g_free (appt->url);
+ g_free (appt->uid);
+ g_slice_free (struct IndicatorDatetimeAppt, appt);
+ }
+}
+
diff --git a/src/planner.h b/src/planner.h
index 206bfe5..ffe8937 100644
--- a/src/planner.h
+++ b/src/planner.h
@@ -40,8 +40,10 @@ GType indicator_datetime_planner_get_type (void);
struct IndicatorDatetimeAppt
{
- char * color;
- char * summary;
+ gchar * color;
+ gchar * summary;
+ gchar * url;
+ gchar * uid;
GDateTime * begin;
GDateTime * end;
gboolean is_event;
diff --git a/src/service.c b/src/service.c
index ef987d5..08945bc 100644
--- a/src/service.c
+++ b/src/service.c
@@ -24,11 +24,11 @@
#include <glib/gi18n.h>
#include <gio/gio.h>
+#include <libnotify/notify.h>
#include <json-glib/json-glib.h>
#include <url-dispatcher.h>
#include "dbus-shared.h"
-#include "planner-eds.h"
#include "timezone-file.h"
#include "timezone-geoclue.h"
#include "service.h"
@@ -53,6 +53,15 @@ static guint signals[LAST_SIGNAL] = { 0 };
enum
{
+ PROP_0,
+ PROP_PLANNER,
+ PROP_LAST
+};
+
+static GParamSpec * properties[PROP_LAST] = { 0 };
+
+enum
+{
SECTION_HEADER = (1<<0),
SECTION_CALENDAR = (1<<1),
SECTION_APPOINTMENTS = (1<<2),
@@ -119,6 +128,7 @@ struct _IndicatorDatetimeServicePrivate
guint header_timer;
guint timezone_timer;
+ guint alarm_timer;
/* Which year/month to show in the calendar,
and which day should get the cursor.
@@ -388,6 +398,197 @@ start_header_timer (IndicatorDatetimeService * self)
g_date_time_unref (now);
}
+/***
+**** ALARMS
+***/
+
+static void set_alarm_timer (IndicatorDatetimeService * self);
+
+static gboolean
+appointment_has_alarm_url (const struct IndicatorDatetimeAppt * appt)
+{
+ return (appt->has_alarms) &&
+ (appt->url != NULL) &&
+ (g_str_has_prefix (appt->url, "alarm:///"));
+}
+
+static gboolean
+datetimes_have_the_same_minute (GDateTime * a G_GNUC_UNUSED, GDateTime * b G_GNUC_UNUSED)
+{
+ int ay, am, ad;
+ int by, bm, bd;
+
+ g_date_time_get_ymd (a, &ay, &am, &ad);
+ g_date_time_get_ymd (b, &by, &bm, &bd);
+
+ return (ay == by) &&
+ (am == bm) &&
+ (ad == bd) &&
+ (g_date_time_get_hour (a) == g_date_time_get_hour (b)) &&
+ (g_date_time_get_minute (a) == g_date_time_get_minute (b));
+}
+
+static void
+dispatch_alarm_url (const struct IndicatorDatetimeAppt * appt)
+{
+ gchar * str;
+
+ g_return_if_fail (appt != NULL);
+ g_return_if_fail (appointment_has_alarm_url (appt));
+
+ str = g_date_time_format (appt->begin, "%F %T");
+ g_debug ("dispatching url \"%s\" for appointment \"%s\", which begins at %s",
+ appt->url, appt->summary, str);
+ g_free (str);
+
+ url_dispatch_send (appt->url, NULL, NULL);
+}
+
+static void
+on_snap_decided (NotifyNotification * notification G_GNUC_UNUSED,
+ char * action,
+ gpointer gurl)
+{
+ g_debug ("%s: %s", G_STRFUNC, action);
+
+ if (!g_strcmp0 (action, "show"))
+ {
+ const gchar * url = gurl;
+ g_debug ("dispatching url '%s'", url);
+ url_dispatch_send (url, NULL, NULL);
+ }
+}
+
+static void
+show_snap_decision_for_alarm (const struct IndicatorDatetimeAppt * appt)
+{
+ gchar * title;
+ const gchar * body;
+ const gchar * icon_name;
+ NotifyNotification * nn;
+ GError * error;
+
+ title = g_date_time_format (appt->begin,
+ get_terse_time_format_string (appt->begin));
+ body = appt->summary;
+ icon_name = ALARM_CLOCK_ICON_NAME;
+ g_debug ("creating a snap decision with title '%s', body '%s', icon '%s'",
+ title, body, icon_name);
+
+ nn = notify_notification_new (title, body, icon_name);
+ notify_notification_set_hint_string (nn,
+ "x-canonical-snap-decisions",
+ "true");
+ notify_notification_set_hint_string (nn,
+ "x-canonical-private-button-tint",
+ "true");
+ notify_notification_add_action (nn, "show", _("Show"),
+ on_snap_decided, g_strdup(appt->url), g_free);
+ notify_notification_add_action (nn, "dismiss", _("Dismiss"),
+ on_snap_decided, NULL, NULL);
+
+ error = NULL;
+ notify_notification_show (nn, &error);
+ if (error != NULL)
+ {
+ g_warning ("Unable to show alarm '%s' popup: %s", body, error->message);
+ g_error_free (error);
+ dispatch_alarm_url (appt);
+ }
+
+ g_free (title);
+}
+
+static void update_appointment_lists (IndicatorDatetimeService * self);
+
+static gboolean
+on_alarm_timer (gpointer gself)
+{
+ IndicatorDatetimeService * self = INDICATOR_DATETIME_SERVICE (gself);
+ GDateTime * now;
+ GSList * l;
+
+ /* If there are any alarms at the current time, show a snap decision */
+ now = indicator_datetime_service_get_localtime (self);
+ for (l=self->priv->upcoming_appointments; l!=NULL; l=l->next)
+ {
+ const struct IndicatorDatetimeAppt * appt = l->data;
+
+ if (appointment_has_alarm_url (appt))
+ if (datetimes_have_the_same_minute (now, appt->begin))
+ show_snap_decision_for_alarm (appt);
+ }
+ g_date_time_unref (now);
+
+ /* rebuild the alarm list asynchronously.
+ set_upcoming_appointments() will update the alarm timer when this
+ async call is done, so no need to restart the timer here... */
+ update_appointment_lists (self);
+
+ return G_SOURCE_REMOVE;
+}
+
+/* if there are upcoming alarms, set the alarm timer to the nearest one.
+ otherwise, unset the alarm timer. */
+static void
+set_alarm_timer (IndicatorDatetimeService * self)
+{
+ priv_t * p;
+ GDateTime * now;
+ GDateTime * alarm_time;
+ GSList * l;
+
+ p = self->priv;
+ indicator_clear_timer (&p->alarm_timer);
+
+ now = indicator_datetime_service_get_localtime (self);
+
+ /* find the time of the next alarm on our calendar */
+ alarm_time = NULL;
+ for (l=p->upcoming_appointments; l!=NULL; l=l->next)
+ {
+ const struct IndicatorDatetimeAppt * appt = l->data;
+
+ if (appointment_has_alarm_url (appt))
+ if (g_date_time_compare (appt->begin, now) > 0)
+ if (!alarm_time || g_date_time_compare (alarm_time, appt->begin) > 0)
+ alarm_time = appt->begin;
+ }
+
+ /* if there's an upcoming alarm, set a timer to wake up at that time */
+ if (alarm_time != NULL)
+ {
+ GTimeSpan interval_msec;
+ gchar * str;
+ GDateTime * then;
+
+ interval_msec = g_date_time_difference (alarm_time, now);
+ interval_msec += G_USEC_PER_SEC; /* fire a moment after alarm_time */
+ interval_msec /= 1000; /* convert from usec to msec */
+
+ str = g_date_time_format (alarm_time, "%F %T");
+ g_debug ("%s is the next alarm time", str);
+ g_free (str);
+ then = g_date_time_add_seconds (now, interval_msec/1000);
+ str = g_date_time_format (then, "%F %T");
+ g_debug ("%s is when we'll wake up for it", str);
+ g_free (str);
+ g_date_time_unref (then);
+
+ p->alarm_timer = g_timeout_add_full (G_PRIORITY_HIGH,
+ (guint) interval_msec,
+ on_alarm_timer,
+ self,
+ NULL);
+ }
+
+ g_date_time_unref (now);
+}
+
+/***
+****
+***/
+
static void
update_internal_timezone (IndicatorDatetimeService * self)
{
@@ -743,23 +944,36 @@ get_appointment_time_format (struct IndicatorDatetimeAppt * appt,
}
static void
-add_appointments (IndicatorDatetimeService * self, GMenu * menu, gboolean terse)
+add_appointments (IndicatorDatetimeService * self, GMenu * menu, gboolean phone)
{
const int MAX_APPTS = 5;
- GDateTime * now = indicator_datetime_service_get_localtime (self);
+ GDateTime * now;
+ GHashTable * added;
GSList * appts;
GSList * l;
int i;
+ now = indicator_datetime_service_get_localtime (self);
+
+ added = g_hash_table_new (g_str_hash, g_str_equal);
+
/* build appointment menuitems */
appts = self->priv->upcoming_appointments;
for (l=appts, i=0; l!=NULL && i<MAX_APPTS; l=l->next, i++)
{
struct IndicatorDatetimeAppt * appt = l->data;
- char * fmt = get_appointment_time_format (appt, now, self->priv->settings, terse);
- const gint64 unix_time = g_date_time_to_unix (appt->begin);
+ char * fmt;
+ gint64 unix_time;
GMenuItem * menu_item;
+ if (g_hash_table_contains (added, appt->uid))
+ continue;
+
+ g_hash_table_add (added, appt->uid);
+
+ fmt = get_appointment_time_format (appt, now, self->priv->settings, phone);
+ unix_time = g_date_time_to_unix (appt->begin);
+
menu_item = g_menu_item_new (appt->summary, NULL);
if (appt->has_alarms)
@@ -776,15 +990,22 @@ add_appointments (IndicatorDatetimeService * self, GMenu * menu, gboolean terse)
g_menu_item_set_attribute (menu_item, "x-canonical-type",
"s", appt->has_alarms ? "com.canonical.indicator.alarm"
: "com.canonical.indicator.appointment");
- g_menu_item_set_action_and_target_value (menu_item,
- "indicator.activate-planner",
- g_variant_new_int64 (unix_time));
+
+ if (phone)
+ g_menu_item_set_action_and_target_value (menu_item,
+ "indicator.activate-appointment",
+ g_variant_new_string (appt->uid));
+ else
+ g_menu_item_set_action_and_target_value (menu_item,
+ "indicator.activate-planner",
+ g_variant_new_int64 (unix_time));
g_menu_append_item (menu, menu_item);
g_object_unref (menu_item);
g_free (fmt);
}
/* cleanup */
+ g_hash_table_unref (added);
g_date_time_unref (now);
}
@@ -1363,6 +1584,34 @@ on_phone_settings_activated (GSimpleAction * a G_GNUC_UNUSED,
}
static void
+on_activate_appointment (GSimpleAction * a G_GNUC_UNUSED,
+ GVariant * param,
+ gpointer gself)
+{
+ priv_t * p = INDICATOR_DATETIME_SERVICE(gself)->priv;
+ const gchar * uid = g_variant_get_string (param, NULL);
+
+ if (uid != NULL)
+ {
+ const struct IndicatorDatetimeAppt * appt;
+ GSList * l;
+
+ /* find the appointment that matches that uid */
+ for (l=p->upcoming_appointments, appt=NULL; l && !appt; l=l->next)
+ {
+ const struct IndicatorDatetimeAppt * tmp = l->data;
+ if (!g_strcmp0 (uid, tmp->uid))
+ appt = tmp;
+ }
+
+ /* if that appointment's an alarm, dispatch its url */
+ g_debug ("%s: uri '%s'; matching appt is %p", G_STRFUNC, uid, appt);
+ if (appt && appointment_has_alarm_url (appt))
+ dispatch_alarm_url (appt);
+ }
+}
+
+static void
on_phone_clock_activated (GSimpleAction * a G_GNUC_UNUSED,
GVariant * param G_GNUC_UNUSED,
gpointer gself G_GNUC_UNUSED)
@@ -1426,6 +1675,7 @@ init_gactions (IndicatorDatetimeService * self)
{ "activate-phone-settings", on_phone_settings_activated },
{ "activate-phone-clock-app", on_phone_clock_activated },
{ "activate-planner", on_activate_planner, "x", NULL },
+ { "activate-appointment", on_activate_appointment, "s", NULL },
{ "set-location", on_set_location, "s" }
};
@@ -1656,6 +1906,10 @@ set_upcoming_appointments (IndicatorDatetimeService * self,
/* sync the menus/actions */
rebuild_appointments_section_soon (self);
+
+ /* alarm timer is keyed off of the next alarm time,
+ so it needs to be rebuilt when the appointment list changes */
+ set_alarm_timer (self);
}
static void
@@ -1825,6 +2079,45 @@ on_name_lost (GDBusConnection * connection G_GNUC_UNUSED,
***/
static void
+my_get_property (GObject * o,
+ guint property_id,
+ GValue * value,
+ GParamSpec * pspec)
+{
+ IndicatorDatetimeService * self = INDICATOR_DATETIME_SERVICE (o);
+
+ switch (property_id)
+ {
+ case PROP_PLANNER:
+ g_value_set_object (value, self->priv->planner);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (o, property_id, pspec);
+ }
+}
+
+static void
+my_set_property (GObject * o,
+ guint property_id,
+ const GValue * value,
+ GParamSpec * pspec)
+{
+ IndicatorDatetimeService * self = INDICATOR_DATETIME_SERVICE (o);
+
+ switch (property_id)
+ {
+ case PROP_PLANNER:
+ indicator_datetime_service_set_planner (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (o, property_id, pspec);
+ }
+}
+
+
+static void
my_dispose (GObject * o)
{
int i;
@@ -1847,13 +2140,7 @@ my_dispose (GObject * o)
set_detect_location_enabled (self, FALSE);
- if (p->planner != NULL)
- {
- g_signal_handlers_disconnect_by_data (p->planner, self);
- g_clear_object (&p->planner);
- }
- g_clear_pointer (&p->upcoming_appointments, indicator_datetime_planner_free_appointments);
- g_clear_pointer (&p->calendar_appointments, indicator_datetime_planner_free_appointments);
+ indicator_datetime_service_set_planner (self, NULL);
if (p->login1_manager != NULL)
{
@@ -1865,6 +2152,7 @@ my_dispose (GObject * o)
indicator_clear_timer (&p->rebuild_id);
indicator_clear_timer (&p->timezone_timer);
indicator_clear_timer (&p->header_timer);
+ indicator_clear_timer (&p->alarm_timer);
if (p->settings != NULL)
{
@@ -1955,16 +2243,6 @@ indicator_datetime_service_init (IndicatorDatetimeService * self)
p->cancellable = g_cancellable_new ();
/***
- **** Create the planner and listen for changes
- ***/
-
- p->planner = indicator_datetime_planner_eds_new ();
-
- g_signal_connect_swapped (p->planner, "appointments-changed",
- G_CALLBACK(update_appointment_lists), self);
-
-
- /***
**** Create the settings object and listen for changes
***/
@@ -2033,6 +2311,8 @@ indicator_datetime_service_init (IndicatorDatetimeService * self)
on_local_time_jumped (self);
+ set_alarm_timer (self);
+
for (i=0; i<N_PROFILES; ++i)
create_menu (self, i);
@@ -2043,9 +2323,12 @@ static void
indicator_datetime_service_class_init (IndicatorDatetimeServiceClass * klass)
{
GObjectClass * object_class = G_OBJECT_CLASS (klass);
+ const GParamFlags flags = G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS;
object_class->dispose = my_dispose;
object_class->finalize = my_finalize;
+ object_class->get_property = my_get_property;
+ object_class->set_property = my_set_property;
g_type_class_add_private (klass, sizeof (IndicatorDatetimeServicePrivate));
@@ -2057,6 +2340,18 @@ indicator_datetime_service_class_init (IndicatorDatetimeServiceClass * klass)
NULL, NULL,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
+
+ /* install properties */
+
+ properties[PROP_0] = NULL;
+
+ properties[PROP_PLANNER] = g_param_spec_object ("planner",
+ "Planner",
+ "The appointment provider",
+ INDICATOR_TYPE_DATETIME_PLANNER,
+ flags);
+
+ g_object_class_install_properties (object_class, PROP_LAST, properties);
}
/***
@@ -2064,9 +2359,11 @@ indicator_datetime_service_class_init (IndicatorDatetimeServiceClass * klass)
***/
IndicatorDatetimeService *
-indicator_datetime_service_new (void)
+indicator_datetime_service_new (IndicatorDatetimePlanner * planner)
{
- GObject * o = g_object_new (INDICATOR_TYPE_DATETIME_SERVICE, NULL);
+ GObject * o = g_object_new (INDICATOR_TYPE_DATETIME_SERVICE,
+ "planner", planner,
+ NULL);
return INDICATOR_DATETIME_SERVICE (o);
}
@@ -2102,3 +2399,38 @@ indicator_datetime_service_set_calendar_date (IndicatorDatetimeService * self,
if (dirty)
update_appointment_lists (self);
}
+
+void
+indicator_datetime_service_set_planner (IndicatorDatetimeService * self,
+ IndicatorDatetimePlanner * planner)
+{
+ priv_t * p;
+
+ g_return_if_fail (INDICATOR_IS_DATETIME_SERVICE (self));
+ g_return_if_fail (INDICATOR_IS_DATETIME_PLANNER (planner));
+
+ p = self->priv;
+
+ /* clear the old planner & appointments */
+
+ if (p->planner != NULL)
+ {
+ g_signal_handlers_disconnect_by_data (p->planner, self);
+ g_clear_object (&p->planner);
+ }
+
+ g_clear_pointer (&p->upcoming_appointments, indicator_datetime_planner_free_appointments);
+ g_clear_pointer (&p->calendar_appointments, indicator_datetime_planner_free_appointments);
+
+ /* set the new planner & begin fetching appointments from it */
+
+ if (planner != NULL)
+ {
+ p->planner = g_object_ref (planner);
+
+ g_signal_connect_swapped (p->planner, "appointments-changed",
+ G_CALLBACK(update_appointment_lists), self);
+
+ update_appointment_lists (self);
+ }
+}
diff --git a/src/service.h b/src/service.h
index b142882..25bb59a 100644
--- a/src/service.h
+++ b/src/service.h
@@ -22,6 +22,7 @@
#include <glib.h>
#include <glib-object.h>
+#include "planner.h"
G_BEGIN_DECLS
@@ -62,13 +63,17 @@ struct _IndicatorDatetimeServiceClass
GType indicator_datetime_service_get_type (void);
-IndicatorDatetimeService * indicator_datetime_service_new (void);
+IndicatorDatetimeService * indicator_datetime_service_new (IndicatorDatetimePlanner * planner);
GDateTime * indicator_datetime_service_get_localtime (IndicatorDatetimeService * service);
void indicator_datetime_service_set_calendar_date (IndicatorDatetimeService * self,
GDateTime * date);
+void indicator_datetime_service_set_planner (IndicatorDatetimeService * self,
+ IndicatorDatetimePlanner * planner);
+
+
G_END_DECLS