diff --git a/data/dbus-interfaces/org.gnome.Shell.CalendarServer.xml b/data/dbus-interfaces/org.gnome.Shell.CalendarServer.xml index c19883095..3005cbac0 100644 --- a/data/dbus-interfaces/org.gnome.Shell.CalendarServer.xml +++ b/data/dbus-interfaces/org.gnome.Shell.CalendarServer.xml @@ -1,12 +1,19 @@ - - - - - + + + + + + + + + + + + + - diff --git a/js/ui/calendar.js b/js/ui/calendar.js index 524d2dd32..250480d79 100644 --- a/js/ui/calendar.js +++ b/js/ui/calendar.js @@ -220,7 +220,9 @@ class DBusEventSource extends EventSourceBase { } } - this._dbusProxy.connectSignal('Changed', this._onChanged.bind(this)); + this._dbusProxy.connectSignal('EventsAdded', this._onEventsAdded.bind(this)); + this._dbusProxy.connectSignal('EventsRemoved', this._onEventsRemoved.bind(this)); + this._dbusProxy.connectSignal('ClientDisappeared', this._onClientDisappeared.bind(this)); this._dbusProxy.connect('notify::g-name-owner', () => { if (this._dbusProxy.g_name_owner) @@ -257,7 +259,7 @@ class DBusEventSource extends EventSourceBase { } _resetCache() { - this._events = []; + this._events = new Map(); this._lastRequestBegin = null; this._lastRequestEnd = null; } @@ -273,28 +275,59 @@ class DBusEventSource extends EventSourceBase { this.emit('changed'); } - _onChanged() { - this._loadEvents(false); - } + _onEventsAdded(dbusProxy, nameOwner, argArray) { + let appointments = argArray[0] || []; + let changed = false; - _onEventsReceived(results, _error) { - let newEvents = []; - let appointments = results[0] || []; for (let n = 0; n < appointments.length; n++) { let a = appointments[n]; - let date = new Date(a[4] * 1000); - let end = new Date(a[5] * 1000); let id = a[0]; let summary = a[1]; - let allDay = a[3]; + let allDay = a[2]; + let date = new Date(a[3] * 1000); + let end = new Date(a[4] * 1000); let event = new CalendarEvent(id, date, end, summary, allDay); - newEvents.push(event); - } - newEvents.sort((ev1, ev2) => ev1.date.getTime() - ev2.date.getTime()); + this._events.set(event.id, event); - this._events = newEvents; - this._isLoading = false; - this.emit('changed'); + changed = true; + } + + if (changed) + this.emit('changed'); + } + + _onEventsRemoved(dbusProxy, nameOwner, argArray) { + let ids = argArray[0] || []; + let changed = false; + + for (let n = 0; n < ids.length; n++) { + let id = ids[n]; + + if (this._events.delete(id)) + changed = true; + } + + if (changed) + this.emit('changed'); + } + + _onClientDisappeared(dbusProxy, nameOwner, argArray) { + let sourceUid = argArray[0] || ""; + let changed = false; + let idsIter = this._events.keys(); + + sourceUid += '\n'; + + for (let item = idsIter.next(); !item.done; item = idsIter.next()) { + let id = item.value; + + if (id.startsWith(sourceUid) && + this._events.delete(id)) + changed = true; + } + + if (changed) + this.emit('changed'); } _loadEvents(forceReload) { @@ -303,32 +336,40 @@ class DBusEventSource extends EventSourceBase { return; if (this._curRequestBegin && this._curRequestEnd) { - this._dbusProxy.GetEventsRemote(this._curRequestBegin.getTime() / 1000, - this._curRequestEnd.getTime() / 1000, - forceReload, - this._onEventsReceived.bind(this), - Gio.DBusCallFlags.NONE); + if (forceReload) { + this._events.clear(); + this.emit('changed'); + } + this._dbusProxy.SetTimeRangeRemote(this._curRequestBegin.getTime() / 1000, + this._curRequestEnd.getTime() / 1000, + forceReload, + Gio.DBusCallFlags.NONE); } } requestRange(begin, end) { if (!(_datesEqual(begin, this._lastRequestBegin) && _datesEqual(end, this._lastRequestEnd))) { - this._isLoading = true; this._lastRequestBegin = begin; this._lastRequestEnd = end; this._curRequestBegin = begin; this._curRequestEnd = end; - this._loadEvents(false); + this._loadEvents(true); } } - getEvents(begin, end) { + getEvents(begin, end, onlyCheckExistence) { let result = []; - for (let n = 0; n < this._events.length; n++) { - let event = this._events[n]; + let eventsIter = this._events.values(); - if (_dateIntervalsOverlap(event.date, event.end, begin, end)) + for (let item = eventsIter.next(); !item.done; item = eventsIter.next()) { + let event = item.value; + + if (_dateIntervalsOverlap(event.date, event.end, begin, end)) { result.push(event); + + if (onlyCheckExistence) + return result; + } } result.sort((event1, event2) => { // sort events by end time on ending day @@ -343,7 +384,7 @@ class DBusEventSource extends EventSourceBase { let dayBegin = _getBeginningOfDay(day); let dayEnd = _getEndOfDay(day); - let events = this.getEvents(dayBegin, dayEnd); + let events = this.getEvents(dayBegin, dayEnd, true); if (events.length == 0) return false; @@ -873,7 +914,7 @@ class EventsSection extends MessageList.MessageListSection { } _reloadEvents() { - if (this._eventSource.isLoading) + if (this._eventSource.isLoading || this._reloading) return; this._reloading = true; @@ -882,10 +923,7 @@ class EventsSection extends MessageList.MessageListSection { let periodEnd = _getEndOfDay(this._date); let events = this._eventSource.getEvents(periodBegin, periodEnd); - let ids = events.map(e => e.id); this._messageById.forEach((message, id) => { - if (ids.includes(id)) - return; this._messageById.delete(id); this.removeMessage(message); }); diff --git a/meson.build b/meson.build index 91193221c..4d63811e4 100644 --- a/meson.build +++ b/meson.build @@ -19,7 +19,7 @@ cogl_pango_pc = 'mutter-cogl-pango-' + mutter_api_version libmutter_pc = 'libmutter-' + mutter_api_version ecal_req = '>= 3.33.1' -eds_req = '>= 3.17.2' +eds_req = '>= 3.33.1' gcr_req = '>= 3.7.5' gio_req = '>= 2.56.0' gi_req = '>= 1.49.1' diff --git a/po/POTFILES.in b/po/POTFILES.in index 8afd84a9b..cd6a02330 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -74,6 +74,7 @@ js/ui/windowAttentionHandler.js js/ui/windowManager.js js/ui/windowMenu.js src/calendar-server/evolution-calendar.desktop.in +src/calendar-server/reminder-watcher.c src/main.c src/shell-app.c src/shell-app-system.c diff --git a/src/calendar-server/calendar-sources.c b/src/calendar-server/calendar-sources.c index a0da1cedf..bb099dc2b 100644 --- a/src/calendar-server/calendar-sources.c +++ b/src/calendar-server/calendar-sources.c @@ -43,20 +43,7 @@ struct _ClientData { ECalClient *client; gulong backend_died_id; -}; - -struct _CalendarSourceData -{ - ECalClientSourceType source_type; - CalendarSources *sources; - guint changed_signal; - - /* ESource -> EClient */ - GHashTable *clients; - - guint timeout_id; - - guint loaded : 1; + gboolean is_for_events; /* Because this can hold other clients too (for EReminderWatcher) */ }; typedef struct _CalendarSourcesPrivate CalendarSourcesPrivate; @@ -69,175 +56,150 @@ struct _CalendarSources struct _CalendarSourcesPrivate { - ESourceRegistry *registry; - gulong source_added_id; - gulong source_changed_id; - gulong source_removed_id; + ESourceRegistryWatcher *registry_watcher; + gulong filter_id; + gulong appeared_id; + gulong disappeared_id; - CalendarSourceData appointment_sources; - CalendarSourceData task_sources; + GMutex clients_lock; + GHashTable *clients; /* ESource -> ClientData */ }; G_DEFINE_TYPE_WITH_PRIVATE (CalendarSources, calendar_sources, G_TYPE_OBJECT) -static void calendar_sources_finalize (GObject *object); - -static void backend_died_cb (EClient *client, CalendarSourceData *source_data); -static void calendar_sources_registry_source_changed_cb (ESourceRegistry *registry, - ESource *source, - CalendarSources *sources); -static void calendar_sources_registry_source_removed_cb (ESourceRegistry *registry, - ESource *source, - CalendarSources *sources); - enum { - APPOINTMENT_SOURCES_CHANGED, - TASK_SOURCES_CHANGED, + CLIENT_APPEARED, + CLIENT_DISAPPEARED, LAST_SIGNAL }; static guint signals [LAST_SIGNAL] = { 0, }; -static GObjectClass *parent_class = NULL; -static CalendarSources *calendar_sources_singleton = NULL; +static void +calendar_sources_client_connected_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + CalendarSources *sources = CALENDAR_SOURCES (source_object); + ESource *source = user_data; + EClient *client; + GError *error = NULL; + + /* The calendar_sources_connect_client_sync() already stored the 'client' + * into the priv->clients */ + client = calendar_sources_connect_client_finish (sources, result, &error); + if (error) + { + g_warning ("Could not load source '%s': %s", + e_source_get_uid (source), + error->message); + g_clear_error (&error); + } + else + { + g_signal_emit (sources, signals[CLIENT_APPEARED], 0, client, NULL); + } + + g_clear_object (&client); + g_clear_object (&source); +} + +static gboolean +registry_watcher_filter_cb (ESourceRegistryWatcher *watcher, + ESource *source, + CalendarSources *sources) +{ + return e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR) && + e_source_selectable_get_selected (e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR)); +} + +static void +registry_watcher_source_appeared_cb (ESourceRegistryWatcher *watcher, + ESource *source, + CalendarSources *sources) +{ + ECalClientSourceType source_type; + + if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR)) + source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS; + else if (e_source_has_extension (source, E_SOURCE_EXTENSION_MEMO_LIST)) + source_type = E_CAL_CLIENT_SOURCE_TYPE_MEMOS; + else if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST)) + source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS; + else + g_return_if_reached (); + + calendar_sources_connect_client (sources, TRUE, source, source_type, 30, NULL, calendar_sources_client_connected_cb, g_object_ref (source)); +} + +static void +registry_watcher_source_disappeared_cb (ESourceRegistryWatcher *watcher, + ESource *source, + CalendarSources *sources) +{ + gboolean emit; + + g_mutex_lock (&sources->priv->clients_lock); + + emit = g_hash_table_remove (sources->priv->clients, source); + + g_mutex_unlock (&sources->priv->clients_lock); + + if (emit) + g_signal_emit (sources, signals[CLIENT_DISAPPEARED], 0, e_source_get_uid (source), NULL); +} static void client_data_free (ClientData *data) { - g_clear_signal_handler (&data->backend_died_id, data->client); + g_signal_handler_disconnect (data->client, data->backend_died_id); g_object_unref (data->client); g_slice_free (ClientData, data); } static void -calendar_sources_class_init (CalendarSourcesClass *klass) -{ - GObjectClass *gobject_class = (GObjectClass *) klass; - - parent_class = g_type_class_peek_parent (klass); - - gobject_class->finalize = calendar_sources_finalize; - - signals [APPOINTMENT_SOURCES_CHANGED] = - g_signal_new ("appointment-sources-changed", - G_TYPE_FROM_CLASS (gobject_class), - G_SIGNAL_RUN_LAST, - 0, - NULL, - NULL, - NULL, - G_TYPE_NONE, - 0); - - signals [TASK_SOURCES_CHANGED] = - g_signal_new ("task-sources-changed", - G_TYPE_FROM_CLASS (gobject_class), - G_SIGNAL_RUN_LAST, - 0, - NULL, - NULL, - NULL, - G_TYPE_NONE, - 0); -} - -static void -calendar_sources_init (CalendarSources *sources) +calendar_sources_constructed (GObject *object) { + CalendarSources *sources = CALENDAR_SOURCES (object); + ESourceRegistry *registry = NULL; GError *error = NULL; - GDBusConnection *session_bus; - GVariant *result; - sources->priv = calendar_sources_get_instance_private (sources); - - /* WORKAROUND: the hardcoded timeout for e_source_registry_new_sync() - (and other library calls that eventually call g_dbus_proxy_new[_sync]()) - is 25 seconds. This has been shown to be too small for - evolution-source-registry in certain cases (slow disk, concurrent IO, - many configured sources), so we first ensure that the service - starts with a manual call and a higher timeout. - - HACK: every time the DBus API is bumped in e-d-s we need - to update this! - */ - session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); - if (session_bus == NULL) - { - g_error ("Failed to connect to the session bus: %s", error->message); - } - - result = g_dbus_connection_call_sync (session_bus, "org.freedesktop.DBus", - "/", "org.freedesktop.DBus", - "StartServiceByName", - g_variant_new ("(su)", - "org.gnome.evolution.dataserver.Sources5", - 0), - NULL, - G_DBUS_CALL_FLAGS_NONE, - 60 * 1000, - NULL, &error); - if (result != NULL) - { - g_variant_unref (result); - sources->priv->registry = e_source_registry_new_sync (NULL, &error); - } + G_OBJECT_CLASS (calendar_sources_parent_class)->constructed (object); + registry = e_source_registry_new_sync (NULL, &error); if (error != NULL) { /* Any error is fatal, but we don't want to crash gnome-shell-calendar-server because of e-d-s problems. So just exit here. */ g_warning ("Failed to start evolution-source-registry: %s", error->message); - exit(EXIT_FAILURE); + exit (EXIT_FAILURE); } - g_object_unref (session_bus); + g_return_if_fail (registry != NULL); - sources->priv->source_added_id = g_signal_connect (sources->priv->registry, - "source-added", - G_CALLBACK (calendar_sources_registry_source_changed_cb), - sources); - sources->priv->source_changed_id = g_signal_connect (sources->priv->registry, - "source-changed", - G_CALLBACK (calendar_sources_registry_source_changed_cb), - sources); - sources->priv->source_removed_id = g_signal_connect (sources->priv->registry, - "source-removed", - G_CALLBACK (calendar_sources_registry_source_removed_cb), - sources); + sources->priv->registry_watcher = e_source_registry_watcher_new (registry, NULL); - sources->priv->appointment_sources.source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS; - sources->priv->appointment_sources.sources = sources; - sources->priv->appointment_sources.changed_signal = signals [APPOINTMENT_SOURCES_CHANGED]; - sources->priv->appointment_sources.clients = g_hash_table_new_full ((GHashFunc) e_source_hash, - (GEqualFunc) e_source_equal, - (GDestroyNotify) g_object_unref, - (GDestroyNotify) client_data_free); - sources->priv->appointment_sources.timeout_id = 0; + g_clear_object (®istry); - sources->priv->task_sources.source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS; - sources->priv->task_sources.sources = sources; - sources->priv->task_sources.changed_signal = signals [TASK_SOURCES_CHANGED]; - sources->priv->task_sources.clients = g_hash_table_new_full ((GHashFunc) e_source_hash, - (GEqualFunc) e_source_equal, - (GDestroyNotify) g_object_unref, - (GDestroyNotify) client_data_free); - sources->priv->task_sources.timeout_id = 0; -} + sources->priv->clients = g_hash_table_new_full ((GHashFunc) e_source_hash, + (GEqualFunc) e_source_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) client_data_free); + sources->priv->filter_id = g_signal_connect (sources->priv->registry_watcher, + "filter", + G_CALLBACK (registry_watcher_filter_cb), + sources); + sources->priv->appeared_id = g_signal_connect (sources->priv->registry_watcher, + "appeared", + G_CALLBACK (registry_watcher_source_appeared_cb), + sources); + sources->priv->disappeared_id = g_signal_connect (sources->priv->registry_watcher, + "disappeared", + G_CALLBACK (registry_watcher_source_disappeared_cb), + sources); -static void -calendar_sources_finalize_source_data (CalendarSources *sources, - CalendarSourceData *source_data) -{ - if (source_data->loaded) - { - g_hash_table_destroy (source_data->clients); - source_data->clients = NULL; - - g_clear_handle_id (&source_data->timeout_id, g_source_remove); - - source_data->loaded = FALSE; - } + e_source_registry_watcher_reclaim (sources->priv->registry_watcher); } static void @@ -245,28 +207,73 @@ calendar_sources_finalize (GObject *object) { CalendarSources *sources = CALENDAR_SOURCES (object); - if (sources->priv->registry) + if (sources->priv->clients) { - g_clear_signal_handler (&sources->priv->source_added_id, - sources->priv->registry); - g_clear_signal_handler (&sources->priv->source_changed_id, - sources->priv->registry); - g_clear_signal_handler (&sources->priv->source_removed_id, - sources->priv->registry); - g_object_unref (sources->priv->registry); + g_hash_table_destroy (sources->priv->clients); + sources->priv->clients = NULL; } - sources->priv->registry = NULL; - calendar_sources_finalize_source_data (sources, &sources->priv->appointment_sources); - calendar_sources_finalize_source_data (sources, &sources->priv->task_sources); + if (sources->priv->registry_watcher) + { + g_signal_handler_disconnect (sources->priv->registry_watcher, + sources->priv->filter_id); + g_signal_handler_disconnect (sources->priv->registry_watcher, + sources->priv->appeared_id); + g_signal_handler_disconnect (sources->priv->registry_watcher, + sources->priv->disappeared_id); + g_clear_object (&sources->priv->registry_watcher); + } - if (G_OBJECT_CLASS (parent_class)->finalize) - G_OBJECT_CLASS (parent_class)->finalize (object); + g_mutex_clear (&sources->priv->clients_lock); + + G_OBJECT_CLASS (calendar_sources_parent_class)->finalize (object); +} + +static void +calendar_sources_class_init (CalendarSourcesClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + gobject_class->constructed = calendar_sources_constructed; + gobject_class->finalize = calendar_sources_finalize; + + signals [CLIENT_APPEARED] = + g_signal_new ("client-appeared", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + E_TYPE_CAL_CLIENT); + + signals [CLIENT_DISAPPEARED] = + g_signal_new ("client-disappeared", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_STRING); /* ESource::uid of the disappeared client */ +} + +static void +calendar_sources_init (CalendarSources *sources) +{ + sources->priv = calendar_sources_get_instance_private (sources); + + g_mutex_init (&sources->priv->clients_lock); } CalendarSources * calendar_sources_get (void) { + static CalendarSources *calendar_sources_singleton = NULL; gpointer singleton_location = &calendar_sources_singleton; if (calendar_sources_singleton) @@ -274,85 +281,70 @@ calendar_sources_get (void) calendar_sources_singleton = g_object_new (CALENDAR_TYPE_SOURCES, NULL); g_object_add_weak_pointer (G_OBJECT (calendar_sources_singleton), - singleton_location); + singleton_location); return calendar_sources_singleton; } -/* The clients are just created here but not loaded */ -static void -create_client_for_source (ESource *source, - ECalClientSourceType source_type, - CalendarSourceData *source_data) +ESourceRegistry * +calendar_sources_get_registry (CalendarSources *sources) { - ClientData *data; - EClient *client; - GError *error = NULL; - - client = g_hash_table_lookup (source_data->clients, source); - g_return_if_fail (client == NULL); - - client = e_cal_client_connect_sync (source, source_type, -1, NULL, &error); - if (!client) - { - g_warning ("Could not load source '%s': %s", - e_source_get_uid (source), - error->message); - g_clear_error(&error); - return; - } - - data = g_slice_new0 (ClientData); - data->client = E_CAL_CLIENT (client); /* takes ownership */ - data->backend_died_id = g_signal_connect (client, - "backend-died", - G_CALLBACK (backend_died_cb), - source_data); - - g_hash_table_insert (source_data->clients, g_object_ref (source), data); -} - -static inline void -debug_dump_ecal_list (GHashTable *clients) -{ -#ifdef CALENDAR_ENABLE_DEBUG - GList *list, *link; - - dprintf ("Loaded clients:\n"); - list = g_hash_table_get_keys (clients); - for (link = list; link != NULL; link = g_list_next (link)) - { - ESource *source = E_SOURCE (link->data); - - dprintf (" %s %s\n", - e_source_get_uid (source), - e_source_get_display_name (source)); - } - g_list_free (list); -#endif + return e_source_registry_watcher_get_registry (sources->priv->registry_watcher); } static void -calendar_sources_load_esource_list (ESourceRegistry *registry, - CalendarSourceData *source_data); - -static gboolean -backend_restart (gpointer data) +gather_event_clients_cb (gpointer key, + gpointer value, + gpointer user_data) { - CalendarSourceData *source_data = data; - ESourceRegistry *registry; + GSList **plist = user_data; + ClientData *cd = value; - registry = source_data->sources->priv->registry; - calendar_sources_load_esource_list (registry, source_data); - g_signal_emit (source_data->sources, source_data->changed_signal, 0); + if (cd && cd->is_for_events) + *plist = g_slist_prepend (*plist, g_object_ref (cd->client)); +} - source_data->timeout_id = 0; - - return FALSE; +GSList * +calendar_sources_ref_clients (CalendarSources *sources) +{ + GSList *list = NULL; + + g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL); + + g_mutex_lock (&sources->priv->clients_lock); + g_hash_table_foreach (sources->priv->clients, gather_event_clients_cb, &list); + g_mutex_unlock (&sources->priv->clients_lock); + + return list; +} + +gboolean +calendar_sources_has_clients (CalendarSources *sources) +{ + GHashTableIter iter; + gpointer value; + gboolean has = FALSE; + + g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), FALSE); + + g_mutex_lock (&sources->priv->clients_lock); + + g_hash_table_iter_init (&iter, sources->priv->clients); + while (!has && g_hash_table_iter_next (&iter, NULL, &value)) + { + ClientData *cd = value; + + has = cd && cd->is_for_events; + } + + g_mutex_unlock (&sources->priv->clients_lock); + + return has; } static void -backend_died_cb (EClient *client, CalendarSourceData *source_data) +backend_died_cb (EClient *client, + CalendarSources *sources) { ESource *source; const char *display_name; @@ -360,196 +352,179 @@ backend_died_cb (EClient *client, CalendarSourceData *source_data) source = e_client_get_source (client); display_name = e_source_get_display_name (source); g_warning ("The calendar backend for '%s' has crashed.", display_name); - g_hash_table_remove (source_data->clients, source); - - g_clear_handle_id (&source_data->timeout_id, g_source_remove); - - source_data->timeout_id = g_timeout_add_seconds (2, backend_restart, - source_data); - g_source_set_name_by_id (source_data->timeout_id, "[gnome-shell] backend_restart"); + g_mutex_lock (&sources->priv->clients_lock); + g_hash_table_remove (sources->priv->clients, source); + g_mutex_unlock (&sources->priv->clients_lock); } -static void -calendar_sources_load_esource_list (ESourceRegistry *registry, - CalendarSourceData *source_data) +EClient * +calendar_sources_connect_client_sync (CalendarSources *sources, + gboolean is_for_events, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GError **error) { - GList *list, *link; - const gchar *extension_name; + EClient *client = NULL; + ClientData *client_data; - switch (source_data->source_type) + g_mutex_lock (&sources->priv->clients_lock); + client_data = g_hash_table_lookup (sources->priv->clients, source); + if (client_data) { + if (is_for_events) + client_data->is_for_events = TRUE; + client = E_CLIENT (g_object_ref (client_data->client)); + } + g_mutex_unlock (&sources->priv->clients_lock); + + if (client) + return client; + + client = e_cal_client_connect_sync (source, source_type, wait_for_connected_seconds, cancellable, error); + if (!client) + return NULL; + + g_mutex_lock (&sources->priv->clients_lock); + client_data = g_hash_table_lookup (sources->priv->clients, source); + if (client_data) { - case E_CAL_CLIENT_SOURCE_TYPE_EVENTS: - extension_name = E_SOURCE_EXTENSION_CALENDAR; - break; - case E_CAL_CLIENT_SOURCE_TYPE_TASKS: - extension_name = E_SOURCE_EXTENSION_TASK_LIST; - break; - default: - g_return_if_reached (); + if (is_for_events) + client_data->is_for_events = TRUE; + g_clear_object (&client); + client = E_CLIENT (g_object_ref (client_data->client)); } - - list = e_source_registry_list_sources (registry, extension_name); - - for (link = list; link != NULL; link = g_list_next (link)) + else { - ESource *source = E_SOURCE (link->data); - ESourceSelectable *extension; - gboolean show_source; + client_data = g_slice_new0 (ClientData); + client_data->client = E_CAL_CLIENT (g_object_ref (client)); + client_data->is_for_events = is_for_events; + client_data->backend_died_id = g_signal_connect (client, + "backend-died", + G_CALLBACK (backend_died_cb), + sources); - extension = e_source_get_extension (source, extension_name); - show_source = e_source_get_enabled (source) && e_source_selectable_get_selected (extension); - - if (show_source) - create_client_for_source (source, source_data->source_type, source_data); + g_hash_table_insert (sources->priv->clients, g_object_ref (source), client_data); } + g_mutex_unlock (&sources->priv->clients_lock); - debug_dump_ecal_list (source_data->clients); - - g_list_free_full (list, g_object_unref); + return client; } +typedef struct _AsyncContext { + gboolean is_for_events; + ESource *source; + ECalClientSourceType source_type; + guint32 wait_for_connected_seconds; +} AsyncContext; + static void -calendar_sources_registry_source_changed_cb (ESourceRegistry *registry, - ESource *source, - CalendarSources *sources) +async_context_free (gpointer ptr) { - if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR)) + AsyncContext *ctx = ptr; + + if (ctx) { - CalendarSourceData *source_data; - ESourceSelectable *extension; - gboolean have_client; - gboolean show_source; - - source_data = &sources->priv->appointment_sources; - extension = e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR); - have_client = (g_hash_table_lookup (source_data->clients, source) != NULL); - show_source = e_source_get_enabled (source) && e_source_selectable_get_selected (extension); - - if (!show_source && have_client) - { - g_hash_table_remove (source_data->clients, source); - g_signal_emit (sources, source_data->changed_signal, 0); - } - if (show_source && !have_client) - { - create_client_for_source (source, source_data->source_type, source_data); - g_signal_emit (sources, source_data->changed_signal, 0); - } - } - - if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST)) - { - CalendarSourceData *source_data; - ESourceSelectable *extension; - gboolean have_client; - gboolean show_source; - - source_data = &sources->priv->task_sources; - extension = e_source_get_extension (source, E_SOURCE_EXTENSION_TASK_LIST); - have_client = (g_hash_table_lookup (source_data->clients, source) != NULL); - show_source = e_source_get_enabled (source) && e_source_selectable_get_selected (extension); - - if (!show_source && have_client) - { - g_hash_table_remove (source_data->clients, source); - g_signal_emit (sources, source_data->changed_signal, 0); - } - if (show_source && !have_client) - { - create_client_for_source (source, source_data->source_type, source_data); - g_signal_emit (sources, source_data->changed_signal, 0); - } + g_clear_object (&ctx->source); + g_slice_free (AsyncContext, ctx); } } static void -calendar_sources_registry_source_removed_cb (ESourceRegistry *registry, - ESource *source, - CalendarSources *sources) +calendar_sources_connect_client_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) { - if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR)) + CalendarSources *sources = source_object; + AsyncContext *ctx = task_data; + EClient *client; + GError *local_error = NULL; + + client = calendar_sources_connect_client_sync (sources, ctx->is_for_events, ctx->source, ctx->source_type, + ctx->wait_for_connected_seconds, cancellable, &local_error); + if (!client) { - CalendarSourceData *source_data; - - source_data = &sources->priv->appointment_sources; - g_hash_table_remove (source_data->clients, source); - g_signal_emit (sources, source_data->changed_signal, 0); - } - - if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST)) - { - CalendarSourceData *source_data; - - source_data = &sources->priv->task_sources; - g_hash_table_remove (source_data->clients, source); - g_signal_emit (sources, source_data->changed_signal, 0); + if (local_error) + g_task_return_error (task, local_error); + else + g_task_return_pointer (task, NULL, NULL); + } else { + g_task_return_pointer (task, client, g_object_unref); } } -static void -ensure_appointment_sources (CalendarSources *sources) +void +calendar_sources_connect_client (CalendarSources *sources, + gboolean is_for_events, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) { - if (!sources->priv->appointment_sources.loaded) + AsyncContext *ctx; + GTask *task; + + ctx = g_slice_new0 (AsyncContext); + ctx->is_for_events = is_for_events; + ctx->source = g_object_ref (source); + ctx->source_type = source_type; + ctx->wait_for_connected_seconds = wait_for_connected_seconds; + + task = g_task_new (sources, cancellable, callback, user_data); + g_task_set_source_tag (task, calendar_sources_connect_client); + g_task_set_task_data (task, ctx, async_context_free); + + g_task_run_in_thread (task, calendar_sources_connect_client_thread); + + g_object_unref (task); +} + +EClient * +calendar_sources_connect_client_finish (CalendarSources *sources, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, sources), NULL); + g_return_val_if_fail (g_async_result_is_tagged (result, calendar_sources_connect_client), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + + +void +print_debug (const gchar *format, + ...) +{ + g_autofree char *s = NULL; + g_autofree char *timestamp = NULL; + va_list ap; + g_autoptr (GDateTime) now = NULL; + static volatile gsize once_init_value = 0; + static gboolean show_debug = FALSE; + static guint pid = 0; + + if (g_once_init_enter (&once_init_value)) { - calendar_sources_load_esource_list (sources->priv->registry, - &sources->priv->appointment_sources); - sources->priv->appointment_sources.loaded = TRUE; + show_debug = (g_getenv ("CALENDAR_SERVER_DEBUG") != NULL); + pid = getpid (); + g_once_init_leave (&once_init_value, 1); } -} - -GList * -calendar_sources_get_appointment_clients (CalendarSources *sources) -{ - GList *list, *link; - - g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL); - - ensure_appointment_sources (sources); - - list = g_hash_table_get_values (sources->priv->appointment_sources.clients); - - for (link = list; link != NULL; link = g_list_next (link)) - link->data = ((ClientData *) link->data)->client; - - return list; -} - -static void -ensure_task_sources (CalendarSources *sources) -{ - if (!sources->priv->task_sources.loaded) - { - calendar_sources_load_esource_list (sources->priv->registry, - &sources->priv->task_sources); - sources->priv->task_sources.loaded = TRUE; - } -} - -GList * -calendar_sources_get_task_clients (CalendarSources *sources) -{ - GList *list, *link; - - g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL); - - ensure_task_sources (sources); - - list = g_hash_table_get_values (sources->priv->task_sources.clients); - - for (link = list; link != NULL; link = g_list_next (link)) - link->data = ((ClientData *) link->data)->client; - - return list; -} - -gboolean -calendar_sources_has_sources (CalendarSources *sources) -{ - g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), FALSE); - - ensure_appointment_sources (sources); - ensure_task_sources (sources); - - return g_hash_table_size (sources->priv->appointment_sources.clients) > 0 || - g_hash_table_size (sources->priv->task_sources.clients) > 0; + + if (!show_debug) + goto out; + + now = g_date_time_new_now_local (); + timestamp = g_date_time_format (now, "%H:%M:%S"); + + va_start (ap, format); + s = g_strdup_vprintf (format, ap); + va_end (ap); + + g_print ("gnome-shell-calendar-server[%d]: %s.%03d: %s\n", + pid, timestamp, g_date_time_get_microsecond (now), s); + out: + ; } diff --git a/src/calendar-server/calendar-sources.h b/src/calendar-server/calendar-sources.h index d11850fff..7645b0641 100644 --- a/src/calendar-server/calendar-sources.h +++ b/src/calendar-server/calendar-sources.h @@ -26,17 +26,46 @@ #include +#define EDS_DISABLE_DEPRECATED +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#include +#include +G_GNUC_END_IGNORE_DEPRECATIONS + G_BEGIN_DECLS #define CALENDAR_TYPE_SOURCES (calendar_sources_get_type ()) G_DECLARE_FINAL_TYPE (CalendarSources, calendar_sources, CALENDAR, SOURCES, GObject) -CalendarSources *calendar_sources_get (void); -GList *calendar_sources_get_appointment_clients (CalendarSources *sources); -GList *calendar_sources_get_task_clients (CalendarSources *sources); +CalendarSources *calendar_sources_get (void); +ESourceRegistry *calendar_sources_get_registry (CalendarSources *sources); +GSList *calendar_sources_ref_clients (CalendarSources *sources); +gboolean calendar_sources_has_clients (CalendarSources *sources); -gboolean calendar_sources_has_sources (CalendarSources *sources); +EClient *calendar_sources_connect_client_sync(CalendarSources *sources, + gboolean is_for_events, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GError **error); +void calendar_sources_connect_client (CalendarSources *sources, + gboolean is_for_events, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +EClient *calendar_sources_connect_client_finish + (CalendarSources *sources, + GAsyncResult *result, + GError **error); + +/* Set the environment variable CALENDAR_SERVER_DEBUG to show debug */ +void print_debug (const gchar *str, + ...) G_GNUC_PRINTF (1, 2); G_END_DECLS diff --git a/src/calendar-server/gnome-shell-calendar-server.c b/src/calendar-server/gnome-shell-calendar-server.c index 2da0f84ba..27c74ddec 100644 --- a/src/calendar-server/gnome-shell-calendar-server.c +++ b/src/calendar-server/gnome-shell-calendar-server.c @@ -40,22 +40,27 @@ G_GNUC_BEGIN_IGNORE_DEPRECATIONS G_GNUC_END_IGNORE_DEPRECATIONS #include "calendar-sources.h" - -/* Set the environment variable CALENDAR_SERVER_DEBUG to show debug */ -static void print_debug (const gchar *str, ...); +#include "reminder-watcher.h" #define BUS_NAME "org.gnome.Shell.CalendarServer" static const gchar introspection_xml[] = "" " " - " " + " " " " " " " " - " " " " - " " + " " /* It's for both added and modified */ + " " + " " + " " + " " + " " + " " + " " + " " " " " " " " @@ -75,35 +80,40 @@ static App *_global_app = NULL; /* ---------------------------------------------------------------------------------------------------- */ -typedef struct +/* While the UID is usually enough to identify an event, + * only the triple of (source,UID,RID) is fully unambiguous; + * neither may contain '\n', so we can safely use it to + * create a unique ID from the triple + */ +static gchar * +create_event_id (const gchar *source_uid, + const gchar *comp_uid, + const gchar *comp_rid) { - char *rid; - time_t start_time; - time_t end_time; -} CalendarOccurrence; - -typedef struct -{ - char *uid; - char *source_id; - char *backend_name; - char *summary; - char *description; - char *color_string; - time_t start_time; - time_t end_time; - guint is_all_day : 1; - - /* Only used internally */ - GSList *occurrences; -} CalendarAppointment; + return g_strconcat ( + source_uid ? source_uid : "", + "\n", + comp_uid ? comp_uid : "", + "\n", + comp_rid ? comp_rid : "", + NULL); +} typedef struct { ECalClient *client; - GHashTable *appointments; + GSList **pappointments; /* CalendarAppointment * */ } CollectAppointmentsData; +typedef struct +{ + gchar *id; + gchar *summary; + time_t start_time; + time_t end_time; + guint is_all_day : 1; +} CalendarAppointment; + static time_t get_time_from_property (ECalClient *cal, ICalComponent *icomp, @@ -142,46 +152,6 @@ get_time_from_property (ECalClient *cal, return retval; } -static char * -get_ical_uid (ICalComponent *icomp) -{ - return g_strdup (i_cal_component_get_uid (icomp)); -} - -static char * -get_ical_summary (ICalComponent *icomp) -{ - ICalProperty *prop; - char *retval; - - prop = i_cal_component_get_first_property (icomp, I_CAL_SUMMARY_PROPERTY); - if (!prop) - return NULL; - - retval = g_strdup (i_cal_property_get_summary (prop)); - - g_object_unref (prop); - - return retval; -} - -static char * -get_ical_description (ICalComponent *icomp) -{ - ICalProperty *prop; - char *retval; - - prop = i_cal_component_get_first_property (icomp, I_CAL_DESCRIPTION_PROPERTY); - if (!prop) - return NULL; - - retval = g_strdup (i_cal_property_get_description (prop)); - - g_object_unref (prop); - - return retval; -} - static inline time_t get_ical_start_time (ECalClient *cal, ICalComponent *icomp, @@ -275,172 +245,48 @@ get_ical_completed_time (ECalClient *cal, default_zone); } -static char * -get_source_color (ECalClient *esource) -{ - ESource *source; - ECalClientSourceType source_type; - ESourceSelectable *extension; - const gchar *extension_name; - - g_return_val_if_fail (E_IS_CAL_CLIENT (esource), NULL); - - source = e_client_get_source (E_CLIENT (esource)); - source_type = e_cal_client_get_source_type (esource); - - switch (source_type) - { - case E_CAL_CLIENT_SOURCE_TYPE_EVENTS: - extension_name = E_SOURCE_EXTENSION_CALENDAR; - break; - case E_CAL_CLIENT_SOURCE_TYPE_TASKS: - extension_name = E_SOURCE_EXTENSION_TASK_LIST; - break; - default: - g_return_val_if_reached (NULL); - } - - extension = e_source_get_extension (source, extension_name); - - return e_source_selectable_dup_color (extension); -} - -static gchar * -get_source_backend_name (ECalClient *esource) -{ - ESource *source; - ECalClientSourceType source_type; - ESourceBackend *extension; - const gchar *extension_name; - - g_return_val_if_fail (E_IS_CAL_CLIENT (esource), NULL); - - source = e_client_get_source (E_CLIENT (esource)); - source_type = e_cal_client_get_source_type (esource); - - switch (source_type) - { - case E_CAL_CLIENT_SOURCE_TYPE_EVENTS: - extension_name = E_SOURCE_EXTENSION_CALENDAR; - break; - case E_CAL_CLIENT_SOURCE_TYPE_TASKS: - extension_name = E_SOURCE_EXTENSION_TASK_LIST; - break; - default: - g_return_val_if_reached (NULL); - } - - extension = e_source_get_extension (source, extension_name); - - return e_source_backend_dup_backend_name (extension); -} - -static inline int -null_safe_strcmp (const char *a, - const char *b) -{ - return (!a && !b) ? 0 : (a && !b) || (!a && b) ? 1 : strcmp (a, b); -} - -static inline gboolean -calendar_appointment_equal (CalendarAppointment *a, - CalendarAppointment *b) -{ - GSList *la, *lb; - - if (g_slist_length (a->occurrences) != g_slist_length (b->occurrences)) - return FALSE; - - for (la = a->occurrences, lb = b->occurrences; la && lb; la = la->next, lb = lb->next) - { - CalendarOccurrence *oa = la->data; - CalendarOccurrence *ob = lb->data; - - if (oa->start_time != ob->start_time || - oa->end_time != ob->end_time || - null_safe_strcmp (oa->rid, ob->rid) != 0) - return FALSE; - } - - return - null_safe_strcmp (a->uid, b->uid) == 0 && - null_safe_strcmp (a->source_id, b->source_id) == 0 && - null_safe_strcmp (a->backend_name, b->backend_name) == 0 && - null_safe_strcmp (a->summary, b->summary) == 0 && - null_safe_strcmp (a->description, b->description) == 0 && - null_safe_strcmp (a->color_string, b->color_string) == 0 && - a->start_time == b->start_time && - a->end_time == b->end_time && - a->is_all_day == b->is_all_day; -} - -static void -calendar_appointment_free (CalendarAppointment *appointment) -{ - GSList *l; - - for (l = appointment->occurrences; l; l = l->next) - g_free (((CalendarOccurrence *)l->data)->rid); - g_slist_free_full (appointment->occurrences, g_free); - appointment->occurrences = NULL; - - g_free (appointment->uid); - appointment->uid = NULL; - - g_free (appointment->source_id); - appointment->source_id = NULL; - - g_free (appointment->backend_name); - appointment->backend_name = NULL; - - g_free (appointment->summary); - appointment->summary = NULL; - - g_free (appointment->description); - appointment->description = NULL; - - g_free (appointment->color_string); - appointment->color_string = NULL; - - appointment->start_time = 0; - appointment->is_all_day = FALSE; -} - -static void -calendar_appointment_init (CalendarAppointment *appointment, - ICalComponent *icomp, - ECalClient *cal) -{ - ICalTimezone *default_zone; - const char *source_id; - - source_id = e_source_get_uid (e_client_get_source (E_CLIENT (cal))); - default_zone = e_cal_client_get_default_timezone (cal); - - appointment->uid = get_ical_uid (icomp); - appointment->source_id = g_strdup (source_id); - appointment->backend_name = get_source_backend_name (cal); - appointment->summary = get_ical_summary (icomp); - appointment->description = get_ical_description (icomp); - appointment->color_string = get_source_color (cal); - appointment->start_time = get_ical_start_time (cal, icomp, default_zone); - appointment->end_time = get_ical_end_time (cal, icomp, default_zone); - appointment->is_all_day = get_ical_is_all_day (cal, - icomp, - appointment->start_time, - default_zone); -} - static CalendarAppointment * -calendar_appointment_new (ICalComponent *icomp, - ECalClient *cal) +calendar_appointment_new (ECalClient *cal, + ECalComponent *comp) { - CalendarAppointment *appointment; + CalendarAppointment *appt; + ICalTimezone *default_zone; + ICalComponent *ical; + ECalComponentId *id; - appointment = g_new0 (CalendarAppointment, 1); + default_zone = e_cal_client_get_default_timezone (cal); + ical = e_cal_component_get_icalcomponent (comp); + id = e_cal_component_get_id (comp); - calendar_appointment_init (appointment, icomp, cal); - return appointment; + appt = g_new0 (CalendarAppointment, 1); + + appt->id = create_event_id (e_source_get_uid (e_client_get_source (E_CLIENT (cal))), + id ? e_cal_component_id_get_uid (id) : NULL, + id ? e_cal_component_id_get_rid (id) : NULL); + appt->summary = g_strdup (i_cal_component_get_summary (ical)); + appt->start_time = get_ical_start_time (cal, ical, default_zone); + appt->end_time = get_ical_end_time (cal, ical, default_zone); + appt->is_all_day = get_ical_is_all_day (cal, + ical, + appt->start_time, + default_zone); + + e_cal_component_id_free (id); + + return appt; +} + +static void +calendar_appointment_free (gpointer ptr) +{ + CalendarAppointment *appt = ptr; + + if (appt) + { + g_free (appt->id); + g_free (appt->summary); + g_free (appt); + } } static time_t @@ -463,34 +309,25 @@ generate_instances_cb (ICalComponent *icomp, GCancellable *cancellable, GError **error) { - ECalClient *cal = ((CollectAppointmentsData *)user_data)->client; - GHashTable *appointments = ((CollectAppointmentsData *)user_data)->appointments; + CollectAppointmentsData *data = user_data; CalendarAppointment *appointment; - CalendarOccurrence *occurrence; + ECalComponent *comp; ICalTimezone *default_zone; - const gchar *uid; - default_zone = e_cal_client_get_default_timezone (cal); - uid = i_cal_component_get_uid (icomp); - appointment = g_hash_table_lookup (appointments, uid); + default_zone = e_cal_client_get_default_timezone (data->client); + comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomp)); - if (appointment == NULL) - { - appointment = calendar_appointment_new (icomp, cal); - g_hash_table_insert (appointments, g_strdup (uid), appointment); - } + appointment = calendar_appointment_new (data->client, comp); + appointment->start_time = timet_from_ical_time (instance_start, default_zone); + appointment->end_time = timet_from_ical_time (instance_end, default_zone); - occurrence = g_new0 (CalendarOccurrence, 1); - occurrence->start_time = timet_from_ical_time (instance_start, default_zone); - occurrence->end_time = timet_from_ical_time (instance_end, default_zone); - occurrence->rid = e_cal_util_component_get_recurid_as_string (icomp); + *(data->pappointments) = g_slist_prepend (*(data->pappointments), appointment); - appointment->occurrences = g_slist_append (appointment->occurrences, occurrence); + g_clear_object (&comp); return TRUE; } - /* ---------------------------------------------------------------------------------------------------- */ struct _App @@ -503,18 +340,19 @@ struct _App ICalTimezone *zone; CalendarSources *sources; - gulong sources_signal_id; + gulong client_appeared_signal_id; + gulong client_disappeared_signal_id; - /* hash from uid to CalendarAppointment objects */ - GHashTable *appointments; + EReminderWatcher *reminder_watcher; gchar *timezone_location; - guint changed_timeout_id; + GSList *notify_appointments; /* CalendarAppointment *, for EventsAdded */ + GSList *notify_ids; /* gchar *, for EventsRemoved */ + guint events_added_timeout_id; + guint events_removed_timeout_id; - gboolean cache_invalid; - - GList *live_views; + GSList *live_views; }; static void @@ -532,6 +370,7 @@ app_update_timezone (App *app) g_free (app->timezone_location); app->timezone_location = location; print_debug ("Using timezone %s", app->timezone_location); + e_reminder_watcher_set_default_zone (app->reminder_watcher, app->zone); } else { @@ -540,38 +379,182 @@ app_update_timezone (App *app) } static gboolean -on_app_schedule_changed_cb (gpointer user_data) +on_app_schedule_events_added_cb (gpointer user_data) { App *app = user_data; - print_debug ("Emitting changed"); + GVariantBuilder builder, extras_builder; + GSList *events, *link; + + if (g_source_is_destroyed (g_main_current_source ())) + return FALSE; + + events = g_slist_reverse (app->notify_appointments); + app->notify_appointments = NULL; + app->events_added_timeout_id = 0; + + print_debug ("Emitting EventsAdded with %d events", g_slist_length (events)); + + if (!events) + return FALSE; + + /* The a{sv} is used as an escape hatch in case we want to provide more + * information in the future without breaking ABI + */ + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ssbxxa{sv})")); + for (link = events; link; link = g_slist_next (link)) + { + CalendarAppointment *appt = link->data; + time_t start_time = appt->start_time; + time_t end_time = appt->end_time; + + if ((start_time >= app->since && + start_time < app->until) || + (start_time <= app->since && + (end_time - 1) > app->since)) + { + g_variant_builder_init (&extras_builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&builder, + "(ssbxxa{sv})", + appt->id, + appt->summary != NULL ? appt->summary : "", + (gboolean) appt->is_all_day, + (gint64) start_time, + (gint64) end_time, + extras_builder); + g_variant_builder_clear (&extras_builder); + } + } + g_dbus_connection_emit_signal (app->connection, NULL, /* destination_bus_name */ "/org/gnome/Shell/CalendarServer", "org.gnome.Shell.CalendarServer", - "Changed", - NULL, /* no params */ + "EventsAdded", + g_variant_new ("(a(ssbxxa{sv}))", &builder), NULL); - app->changed_timeout_id = 0; + + g_variant_builder_clear (&builder); + + g_slist_free_full (events, calendar_appointment_free); + return FALSE; } static void -app_schedule_changed (App *app) +app_schedule_events_added (App *app) { - print_debug ("Scheduling changed"); - if (app->changed_timeout_id == 0) + print_debug ("Scheduling EventsAdded"); + if (app->events_added_timeout_id == 0) { - app->changed_timeout_id = g_timeout_add (2000, - on_app_schedule_changed_cb, - app); - g_source_set_name_by_id (app->changed_timeout_id, "[gnome-shell] on_app_schedule_changed_cb"); + app->events_added_timeout_id = g_timeout_add_seconds (2, + on_app_schedule_events_added_cb, + app); + g_source_set_name_by_id (app->events_added_timeout_id, "[gnome-shell] on_app_schedule_events_added_cb"); + } +} + +static gboolean +on_app_schedule_events_removed_cb (gpointer user_data) +{ + App *app = user_data; + GVariantBuilder builder; + GSList *ids, *link; + + if (g_source_is_destroyed (g_main_current_source ())) + return FALSE; + + ids = app->notify_ids; + app->notify_ids = NULL; + app->events_removed_timeout_id = 0; + + print_debug ("Emitting EventsRemoved with %d ids", g_slist_length (ids)); + + if (!ids) + return FALSE; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("as")); + for (link = ids; link; link = g_slist_next (link)) + { + const gchar *id = link->data; + + g_variant_builder_add (&builder, "s", id); + } + + g_dbus_connection_emit_signal (app->connection, + NULL, /* destination_bus_name */ + "/org/gnome/Shell/CalendarServer", + "org.gnome.Shell.CalendarServer", + "EventsRemoved", + g_variant_new ("(as)", &builder), + NULL); + g_variant_builder_clear (&builder); + + g_slist_free_full (ids, g_free); + + return FALSE; +} + +static void +app_schedule_events_removed (App *app) +{ + print_debug ("Scheduling EventsRemoved"); + if (app->events_removed_timeout_id == 0) + { + app->events_removed_timeout_id = g_timeout_add_seconds (2, + on_app_schedule_events_removed_cb, + app); + g_source_set_name_by_id (app->events_removed_timeout_id, "[gnome-shell] on_app_schedule_events_removed_cb"); } } static void -invalidate_cache (App *app) +app_process_added_modified_objects (App *app, + ECalClientView *view, + GSList *objects) /* ICalComponent * */ { - app->cache_invalid = TRUE; + ECalClient *cal_client; + GSList *link; + gboolean expand_recurrences; + + cal_client = e_cal_client_view_ref_client (view); + expand_recurrences = e_cal_client_get_source_type (cal_client) == E_CAL_CLIENT_SOURCE_TYPE_EVENTS; + + for (link = objects; link; link = g_slist_next (link)) + { + ECalComponent *comp; + ICalComponent *icomp = link->data; + + if (!icomp || !i_cal_component_get_uid (icomp)) + continue; + + if (expand_recurrences && + !e_cal_util_component_is_instance (icomp) && + e_cal_util_component_has_recurrences (icomp)) + { + CollectAppointmentsData data; + + data.client = cal_client; + data.pappointments = &app->notify_appointments; + + e_cal_client_generate_instances_for_object_sync (cal_client, icomp, app->since, app->until, NULL, + generate_instances_cb, &data); + } + else + { + comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomp)); + if (!comp) + continue; + + app->notify_appointments = g_slist_prepend (app->notify_appointments, + calendar_appointment_new (cal_client, comp)); + g_object_unref (comp); + } + } + + g_clear_object (&cal_client); + + if (app->notify_appointments) + app_schedule_events_added (app); } static void @@ -580,24 +563,13 @@ on_objects_added (ECalClientView *view, gpointer user_data) { App *app = user_data; - GSList *l; + ECalClient *client; - print_debug ("%s for calendar", G_STRFUNC); + client = e_cal_client_view_ref_client (view); + print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (objects), e_source_get_uid (e_client_get_source (E_CLIENT (client)))); + g_clear_object (&client); - for (l = objects; l != NULL; l = l->next) - { - ICalComponent *icomp = l->data; - const char *uid; - - uid = i_cal_component_get_uid (icomp); - - if (g_hash_table_lookup (app->appointments, uid) == NULL) - { - /* new appointment we don't know about => changed signal */ - invalidate_cache (app); - app_schedule_changed (app); - } - } + app_process_added_modified_objects (app, view, objects); } static void @@ -606,9 +578,13 @@ on_objects_modified (ECalClientView *view, gpointer user_data) { App *app = user_data; - print_debug ("%s for calendar", G_STRFUNC); - invalidate_cache (app); - app_schedule_changed (app); + ECalClient *client; + + client = e_cal_client_view_ref_client (view); + print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (objects), e_source_get_uid (e_client_get_source (E_CLIENT (client)))); + g_clear_object (&client); + + app_process_added_modified_objects (app, view, objects); } static void @@ -617,42 +593,60 @@ on_objects_removed (ECalClientView *view, gpointer user_data) { App *app = user_data; - print_debug ("%s for calendar", G_STRFUNC); - invalidate_cache (app); - app_schedule_changed (app); + ECalClient *client; + GSList *link; + const gchar *source_uid; + + client = e_cal_client_view_ref_client (view); + source_uid = e_source_get_uid (e_client_get_source (E_CLIENT (client))); + + print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (uids), source_uid); + + for (link = uids; link; link = g_slist_next (link)) + { + ECalComponentId *id = link->data; + + if (!id) + continue; + + app->notify_ids = g_slist_prepend (app->notify_ids, + create_event_id (source_uid, + e_cal_component_id_get_uid (id), + e_cal_component_id_get_rid (id))); + } + + g_clear_object (&client); + + if (app->notify_ids) + app_schedule_events_removed (app); } -static void -app_load_events (App *app) +static gboolean +app_has_calendars (App *app) +{ + return app->live_views != NULL; +} + +static ECalClientView * +app_start_view (App *app, + ECalClient *cal_client) { - GList *clients; - GList *l; - GList *ll; gchar *since_iso8601; gchar *until_iso8601; gchar *query; - const char *tz_location; + const gchar *tz_location; + ECalClientView *view = NULL; + GError *error = NULL; - /* out with the old */ - g_hash_table_remove_all (app->appointments); - /* nuke existing views */ - for (ll = app->live_views; ll != NULL; ll = ll->next) - { - ECalClientView *view = E_CAL_CLIENT_VIEW (ll->data); - g_signal_handlers_disconnect_by_func (view, on_objects_added, app); - g_signal_handlers_disconnect_by_func (view, on_objects_modified, app); - g_signal_handlers_disconnect_by_func (view, on_objects_removed, app); - e_cal_client_view_stop (view, NULL); - g_object_unref (view); - } - g_list_free (app->live_views); - app->live_views = NULL; + if (app->since <= 0 || app->since >= app->until) + return NULL; if (!app->since || !app->until) { print_debug ("Skipping load of events, no time interval set yet"); - return; + return NULL; } + /* timezone could have changed */ app_update_timezone (app); @@ -660,9 +654,10 @@ app_load_events (App *app) until_iso8601 = isodate_from_time_t (app->until); tz_location = i_cal_timezone_get_location (app->zone); - print_debug ("Loading events since %s until %s", + print_debug ("Loading events since %s until %s for calendar '%s'", since_iso8601, - until_iso8601); + until_iso8601, + e_source_get_uid (e_client_get_source (E_CLIENT (cal_client)))); query = g_strdup_printf ("occur-in-time-range? (make-time \"%s\") " "(make-time \"%s\") \"%s\"", @@ -670,113 +665,211 @@ app_load_events (App *app) until_iso8601, tz_location); - clients = calendar_sources_get_appointment_clients (app->sources); - for (l = clients; l != NULL; l = l->next) + e_cal_client_set_default_timezone (cal_client, app->zone); + + if (!e_cal_client_get_view_sync (cal_client, query, &view, NULL /* cancellable */, &error)) { - ECalClient *cal = E_CAL_CLIENT (l->data); - GError *error; - ECalClientView *view; - CollectAppointmentsData data; - - e_cal_client_set_default_timezone (cal, app->zone); - - data.client = cal; - data.appointments = app->appointments; - e_cal_client_generate_instances_sync (cal, - app->since, - app->until, - NULL, - generate_instances_cb, - &data); - - error = NULL; - if (!e_cal_client_get_view_sync (cal, - query, - &view, - NULL, /* cancellable */ - &error)) - { - g_warning ("Error setting up live-query on calendar: %s\n", error->message); - g_error_free (error); - } - else - { - g_signal_connect (view, - "objects-added", - G_CALLBACK (on_objects_added), - app); - g_signal_connect (view, - "objects-modified", - G_CALLBACK (on_objects_modified), - app); - g_signal_connect (view, - "objects-removed", - G_CALLBACK (on_objects_removed), - app); - e_cal_client_view_start (view, NULL); - app->live_views = g_list_prepend (app->live_views, view); - } + g_warning ("Error setting up live-query '%s' on calendar: %s\n", query, error ? error->message : "Unknown error"); + g_clear_error (&error); + view = NULL; } - g_list_free (clients); + else + { + g_signal_connect (view, + "objects-added", + G_CALLBACK (on_objects_added), + app); + g_signal_connect (view, + "objects-modified", + G_CALLBACK (on_objects_modified), + app); + g_signal_connect (view, + "objects-removed", + G_CALLBACK (on_objects_removed), + app); + e_cal_client_view_start (view, NULL); + } + g_free (since_iso8601); g_free (until_iso8601); g_free (query); - app->cache_invalid = FALSE; -} -static gboolean -app_has_calendars (App *app) -{ - return calendar_sources_has_sources (app->sources); + return view; } static void -on_appointment_sources_changed (CalendarSources *sources, - gpointer user_data) +app_stop_view (App *app, + ECalClientView *view) +{ + e_cal_client_view_stop (view, NULL); + + g_signal_handlers_disconnect_by_func (view, on_objects_added, app); + g_signal_handlers_disconnect_by_func (view, on_objects_modified, app); + g_signal_handlers_disconnect_by_func (view, on_objects_removed, app); +} + +static void +app_update_views (App *app) +{ + GSList *link, *clients; + + for (link = app->live_views; link; link = g_slist_next (link)) + { + app_stop_view (app, link->data); + } + + g_slist_free_full (app->live_views, g_object_unref); + app->live_views = NULL; + + clients = calendar_sources_ref_clients (app->sources); + + for (link = clients; link; link = g_slist_next (link)) + { + ECalClient *cal_client = link->data; + ECalClientView *view; + + if (!cal_client) + continue; + + view = app_start_view (app, cal_client); + if (view) + app->live_views = g_slist_prepend (app->live_views, view); + } + + g_slist_free_full (clients, g_object_unref); +} + +static void +app_notify_has_calendars (App *app) +{ + GVariantBuilder dict_builder; + + g_variant_builder_init (&dict_builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&dict_builder, "{sv}", "HasCalendars", + g_variant_new_boolean (app_has_calendars (app))); + + g_dbus_connection_emit_signal (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL), + NULL, + "/org/gnome/Shell/CalendarServer", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new ("(sa{sv}as)", + "org.gnome.Shell.CalendarServer", + &dict_builder, + NULL), + NULL); + g_variant_builder_clear (&dict_builder); +} + +static void +on_client_appeared_cb (CalendarSources *sources, + ECalClient *client, + gpointer user_data) { App *app = user_data; + ECalClientView *view; + GSList *link; + const gchar *source_uid; - print_debug ("Sources changed\n"); - app_load_events (app); + source_uid = e_source_get_uid (e_client_get_source (E_CLIENT (client))); - /* Notify the HasCalendars property */ - { - GVariantBuilder dict_builder; + print_debug ("Client appeared '%s'", source_uid); - g_variant_builder_init (&dict_builder, G_VARIANT_TYPE ("a{sv}")); - g_variant_builder_add (&dict_builder, "{sv}", "HasCalendars", - g_variant_new_boolean (app_has_calendars (app))); + for (link = app->live_views; link; link = g_slist_next (link)) + { + ECalClientView *view = link->data; + ECalClient *cal_client; + ESource *source; - g_dbus_connection_emit_signal (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL), - NULL, - "/org/gnome/Shell/CalendarServer", - "org.freedesktop.DBus.Properties", - "PropertiesChanged", - g_variant_new ("(sa{sv}as)", - "org.gnome.Shell.CalendarServer", - &dict_builder, - NULL), - NULL); - } + cal_client = e_cal_client_view_ref_client (view); + source = e_client_get_source (E_CLIENT (cal_client)); + + if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0) + { + g_clear_object (&cal_client); + return; + } + + g_clear_object (&cal_client); + } + + view = app_start_view (app, client); + + if (view) + { + app->live_views = g_slist_prepend (app->live_views, view); + + /* It's the first view, notify that it has calendars now */ + if (!g_slist_next (app->live_views)) + app_notify_has_calendars (app); + } +} + +static void +on_client_disappeared_cb (CalendarSources *sources, + const gchar *source_uid, + gpointer user_data) +{ + App *app = user_data; + GSList *link; + + print_debug ("Client disappeared '%s'", source_uid); + + for (link = app->live_views; link; link = g_slist_next (link)) + { + ECalClientView *view = link->data; + ECalClient *cal_client; + ESource *source; + + cal_client = e_cal_client_view_ref_client (view); + source = e_client_get_source (E_CLIENT (cal_client)); + + if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0) + { + g_clear_object (&cal_client); + app_stop_view (app, view); + app->live_views = g_slist_remove (app->live_views, view); + g_object_unref (view); + + print_debug ("Emitting ClientDisappeared for '%s'", source_uid); + + g_dbus_connection_emit_signal (app->connection, + NULL, /* destination_bus_name */ + "/org/gnome/Shell/CalendarServer", + "org.gnome.Shell.CalendarServer", + "ClientDisappeared", + g_variant_new ("(s)", source_uid), + NULL); + + /* It was the last view, notify that it doesn't have calendars now */ + if (!app->live_views) + app_notify_has_calendars (app); + + break; + } + + g_clear_object (&cal_client); + } } static App * -app_new (GDBusConnection *connection) +app_new (GApplication *application, + GDBusConnection *connection) { App *app; app = g_new0 (App, 1); app->connection = g_object_ref (connection); app->sources = calendar_sources_get (); - app->sources_signal_id = g_signal_connect (app->sources, - "appointment-sources-changed", - G_CALLBACK (on_appointment_sources_changed), - app); - - app->appointments = g_hash_table_new_full (g_str_hash, - g_str_equal, - g_free, - (GDestroyNotify) calendar_appointment_free); + app->reminder_watcher = reminder_watcher_new (application, calendar_sources_get_registry (app->sources)); + app->client_appeared_signal_id = g_signal_connect (app->sources, + "client-appeared", + G_CALLBACK (on_client_appeared_cb), + app); + app->client_disappeared_signal_id = g_signal_connect (app->sources, + "client-disappeared", + G_CALLBACK (on_client_disappeared_cb), + app); app_update_timezone (app); @@ -786,28 +879,33 @@ app_new (GDBusConnection *connection) static void app_free (App *app) { - GList *ll; - for (ll = app->live_views; ll != NULL; ll = ll->next) + GSList *ll; + + g_clear_handle_id (&app->events_added_timeout_id, g_source_remove); + g_clear_handle_id (&app->events_removed_timeout_id, g_source_remove); + + for (ll = app->live_views; ll != NULL; ll = g_slist_next (ll)) { ECalClientView *view = E_CAL_CLIENT_VIEW (ll->data); - g_signal_handlers_disconnect_by_func (view, on_objects_added, app); - g_signal_handlers_disconnect_by_func (view, on_objects_modified, app); - g_signal_handlers_disconnect_by_func (view, on_objects_removed, app); - e_cal_client_view_stop (view, NULL); - g_object_unref (view); + + app_stop_view (app, view); } - g_list_free (app->live_views); + + g_signal_handler_disconnect (app->sources, + app->client_appeared_signal_id); + g_signal_handler_disconnect (app->sources, + app->client_disappeared_signal_id); g_free (app->timezone_location); - g_hash_table_unref (app->appointments); + g_slist_free_full (app->live_views, g_object_unref); + g_slist_free_full (app->notify_appointments, calendar_appointment_free); + g_slist_free_full (app->notify_ids, g_free); g_object_unref (app->connection); - g_clear_signal_handler (&app->sources_signal_id, app->sources); + g_object_unref (app->reminder_watcher); g_object_unref (app->sources); - g_clear_handle_id (&app->changed_timeout_id, g_source_remove); - g_free (app); } @@ -825,15 +923,12 @@ handle_method_call (GDBusConnection *connection, { App *app = user_data; - if (g_strcmp0 (method_name, "GetEvents") == 0) + if (g_strcmp0 (method_name, "SetTimeRange") == 0) { - GVariantBuilder builder; - GHashTableIter hash_iter; - CalendarAppointment *a; gint64 since; gint64 until; - gboolean force_reload; - gboolean window_changed; + gboolean force_reload = FALSE; + gboolean window_changed = FALSE; g_variant_get (parameters, "(xxb)", @@ -849,13 +944,12 @@ handle_method_call (GDBusConnection *connection, goto out; } - print_debug ("Handling GetEvents (since=%" G_GINT64_FORMAT ", until=%" G_GINT64_FORMAT ", force_reload=%s)", + print_debug ("Handling SetTimeRange (since=%" G_GINT64_FORMAT ", until=%" G_GINT64_FORMAT ", force_reload=%s)", since, until, force_reload ? "true" : "false"); - window_changed = FALSE; - if (!(app->until == until && app->since == since)) + if (app->until != until || app->since != since) { GVariantBuilder *builder; GVariantBuilder *invalidated_builder; @@ -880,61 +974,15 @@ handle_method_call (GDBusConnection *connection, builder, invalidated_builder), NULL); /* GError** */ + + g_variant_builder_unref (builder); + g_variant_builder_unref (invalidated_builder); } - /* reload events if necessary */ - if (window_changed || force_reload || app->cache_invalid) - { - app_load_events (app); - } + g_dbus_method_invocation_return_value (invocation, NULL); - /* The a{sv} is used as an escape hatch in case we want to provide more - * information in the future without breaking ABI - */ - g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(sssbxxa{sv})")); - g_hash_table_iter_init (&hash_iter, app->appointments); - while (g_hash_table_iter_next (&hash_iter, NULL, (gpointer) &a)) - { - GVariantBuilder extras_builder; - GSList *l; - - for (l = a->occurrences; l; l = l->next) - { - CalendarOccurrence *o = l->data; - time_t start_time = o->start_time; - time_t end_time = o->end_time; - - if ((start_time >= app->since && - start_time < app->until) || - (start_time <= app->since && - (end_time - 1) > app->since)) - { - /* While the UID is usually enough to identify an event, - * only the triple of (source,UID,RID) is fully unambiguous; - * neither may contain '\n', so we can safely use it to - * create a unique ID from the triple - */ - char *id = g_strdup_printf ("%s\n%s\n%s", - a->source_id, - a->uid, - o->rid ? o->rid : ""); - - g_variant_builder_init (&extras_builder, G_VARIANT_TYPE ("a{sv}")); - g_variant_builder_add (&builder, - "(sssbxxa{sv})", - id, - a->summary != NULL ? a->summary : "", - a->description != NULL ? a->description : "", - (gboolean) a->is_all_day, - (gint64) start_time, - (gint64) end_time, - extras_builder); - g_free (id); - } - } - } - g_dbus_method_invocation_return_value (invocation, - g_variant_new ("(a(sssbxxa{sv}))", &builder)); + if (window_changed || force_reload) + app_update_views (app); } else { @@ -989,12 +1037,12 @@ on_bus_acquired (GDBusConnection *connection, const gchar *name, gpointer user_data) { - GError *error; + GApplication *application = user_data; guint registration_id; + GError *error = NULL; - _global_app = app_new (connection); + _global_app = app_new (application, connection); - error = NULL; registration_id = g_dbus_connection_register_object (connection, "/org/gnome/Shell/CalendarServer", introspection_data->interfaces[0], @@ -1004,16 +1052,16 @@ on_bus_acquired (GDBusConnection *connection, &error); if (registration_id == 0) { - g_printerr ("Error exporting object: %s (%s %d)", + g_printerr ("Error exporting object: %s (%s %d)\n", error->message, g_quark_to_string (error->domain), error->code); g_error_free (error); - _exit (1); + g_application_quit (application); + return; } print_debug ("Connected to the session bus"); - } static void @@ -1021,11 +1069,11 @@ on_name_lost (GDBusConnection *connection, const gchar *name, gpointer user_data) { - GMainLoop *main_loop = user_data; + GApplication *application = user_data; g_print ("gnome-shell-calendar-server[%d]: Lost (or failed to acquire) the name " BUS_NAME " - exiting\n", (gint) getpid ()); - g_main_loop_quit (main_loop); + g_application_quit (application); } static void @@ -1041,13 +1089,13 @@ stdin_channel_io_func (GIOChannel *source, GIOCondition condition, gpointer data) { - GMainLoop *main_loop = data; + GApplication *application = data; if (condition & G_IO_HUP) { g_debug ("gnome-shell-calendar-server[%d]: Got HUP on stdin - exiting\n", (gint) getpid ()); - g_main_loop_quit (main_loop); + g_application_quit (application); } else { @@ -1060,9 +1108,9 @@ int main (int argc, char **argv) { + GApplication *application; GError *error; GOptionContext *opt_context; - GMainLoop *main_loop; gint ret; guint name_owner_id; GIOChannel *stdin_channel; @@ -1071,6 +1119,8 @@ main (int argc, opt_context = NULL; name_owner_id = 0; stdin_channel = NULL; + application = NULL; + error = NULL; introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); g_assert (introspection_data != NULL); @@ -1080,20 +1130,23 @@ main (int argc, error = NULL; if (!g_option_context_parse (opt_context, &argc, &argv, &error)) { - g_printerr ("Error parsing options: %s", error->message); + g_printerr ("Error parsing options: %s\n", error->message); g_error_free (error); goto out; } - main_loop = g_main_loop_new (NULL, FALSE); + application = g_application_new (BUS_NAME, G_APPLICATION_NON_UNIQUE); + + g_signal_connect (application, "activate", + G_CALLBACK (g_application_hold), NULL); stdin_channel = g_io_channel_unix_new (STDIN_FILENO); g_io_add_watch_full (stdin_channel, G_PRIORITY_DEFAULT, G_IO_HUP, stdin_channel_io_func, - g_main_loop_ref (main_loop), - (GDestroyNotify) g_main_loop_unref); + application, + (GDestroyNotify) NULL); name_owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, BUS_NAME, @@ -1102,14 +1155,22 @@ main (int argc, on_bus_acquired, on_name_acquired, on_name_lost, - g_main_loop_ref (main_loop), - (GDestroyNotify) g_main_loop_unref); + application, + (GDestroyNotify) NULL); - g_main_loop_run (main_loop); + if (g_application_register (application, NULL, &error)) + { + print_debug ("Registered application"); - g_main_loop_unref (main_loop); + ret = g_application_run (application, argc, argv); - ret = 0; + print_debug ("Quit application"); + } + else + { + g_printerr ("Failed to register application: %s\n", error->message); + g_clear_error (&error); + } out: if (stdin_channel != NULL) @@ -1120,41 +1181,7 @@ main (int argc, g_bus_unown_name (name_owner_id); if (opt_context != NULL) g_option_context_free (opt_context); + g_clear_object (&application); + return ret; } - -/* ---------------------------------------------------------------------------------------------------- */ - -static void __attribute__((format(printf, 1, 0))) -print_debug (const gchar *format, ...) -{ - g_autofree char *s = NULL; - g_autofree char *timestamp = NULL; - va_list ap; - g_autoptr (GDateTime) now = NULL; - static volatile gsize once_init_value = 0; - static gboolean show_debug = FALSE; - static guint pid = 0; - - if (g_once_init_enter (&once_init_value)) - { - show_debug = (g_getenv ("CALENDAR_SERVER_DEBUG") != NULL); - pid = getpid (); - g_once_init_leave (&once_init_value, 1); - } - - if (!show_debug) - goto out; - - now = g_date_time_new_now_local (); - timestamp = g_date_time_format (now, "%H:%M:%S"); - - va_start (ap, format); - s = g_strdup_vprintf (format, ap); - va_end (ap); - - g_print ("gnome-shell-calendar-server[%d]: %s.%03d: %s\n", - pid, timestamp, g_date_time_get_microsecond (now), s); - out: - ; -} diff --git a/src/calendar-server/meson.build b/src/calendar-server/meson.build index 7363282a5..ff3bb7535 100644 --- a/src/calendar-server/meson.build +++ b/src/calendar-server/meson.build @@ -2,7 +2,9 @@ calendar_sources = [ 'gnome-shell-calendar-server.c', 'calendar-debug.h', 'calendar-sources.c', - 'calendar-sources.h' + 'calendar-sources.h', + 'reminder-watcher.c', + 'reminder-watcher.h' ] calendar_server = executable('gnome-shell-calendar-server', calendar_sources, diff --git a/src/calendar-server/reminder-watcher.c b/src/calendar-server/reminder-watcher.c new file mode 100644 index 000000000..9e2061720 --- /dev/null +++ b/src/calendar-server/reminder-watcher.c @@ -0,0 +1,706 @@ +/* + * Copyright (C) 2020 Red Hat (www.redhat.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include + +#include + +#define HANDLE_LIBICAL_MEMORY +#define EDS_DISABLE_DEPRECATED +#include + +#include "calendar-sources.h" +#include "reminder-watcher.h" + +struct _ReminderWatcherPrivate { + GApplication *application; /* not referenced */ + CalendarSources *sources; + GSettings *settings; + + GMutex dismiss_lock; + GSList *dismiss; /* EReminderData * */ + GThread *dismiss_thread; /* not referenced, only to know whether it's scheduled */ +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ReminderWatcher, reminder_watcher, E_TYPE_REMINDER_WATCHER) + +static const gchar * +reminder_watcher_get_rd_summary (const EReminderData *rd) +{ + if (!rd) + return NULL; + + return i_cal_component_get_summary (e_cal_component_get_icalcomponent (e_reminder_data_get_component (rd))); +} + +static gboolean +reminder_watcher_notify_audio (ReminderWatcher *rw, + const EReminderData *rd, + ECalComponentAlarm *alarm) +{ +#if 0 + ICalAttach *attach = NULL; + GSList *attachments; + gboolean did_play = FALSE; + + g_return_val_if_fail (rw != NULL, FALSE); + g_return_val_if_fail (rd != NULL, FALSE); + g_return_val_if_fail (alarm != NULL, FALSE); + + attachments = e_cal_component_alarm_get_attachments (alarm); + if (attachments && !attachments->next) + attach = attachments->data; + + if (attach && i_cal_attach_get_is_url (attach)) + { + const gchar *url; + + url = i_cal_attach_get_url (attach); + if (url && *url) + { + gchar *filename; + GError *error = NULL; + + filename = g_filename_from_uri (url, NULL, &error); + + if (!filename) + ean_debug_print ("Audio notify: Failed to convert URI '%s' to filename: %s\n", url, error ? error->message : "Unknown error"); + else if (g_file_test (filename, G_FILE_TEST_EXISTS)) + { +#ifdef HAVE_CANBERRA + gint err = ca_context_play (ca_gtk_context_get (), 0, + CA_PROP_MEDIA_FILENAME, filename, + NULL); + + did_play = !err; + + if (err) + ean_debug_print ("Audio notify: Cannot play file '%s': %s\n", filename, ca_strerror (err)); +#else + ean_debug_print ("Audio notify: Cannot play file '%s': Not compiled with libcanberra\n", filename); +#endif + } + else + ean_debug_print ("Audio notify: File '%s' does not exist\n", filename); + + g_clear_error (&error); + g_free (filename); + } + else + ean_debug_print ("Audio notify: Alarm has stored empty URL, fallback to default sound\n"); + } + else if (!attach) + ean_debug_print ("Audio notify: Alarm has no attachment, fallback to default sound\n"); + else + ean_debug_print ("Audio notify: Alarm attachment is not a URL to sound file, fallback to default sound\n"); + +#ifdef HAVE_CANBERRA + if (!did_play) + { + gint err = ca_context_play (ca_gtk_context_get (), 0, + CA_PROP_EVENT_ID, "alarm-clock-elapsed", + NULL); + + did_play = !err; + + if (err) + ean_debug_print ("Audio notify: Cannot play event sound: %s\n", ca_strerror (err)); + } +#endif + + if (!did_play) + { + GdkDisplay *display; + + display = rw->priv->window ? gtk_widget_get_display (rw->priv->window) : NULL; + + if (!display) + display = gdk_display_get_default (); + + if (display) + gdk_display_beep (display); + else + ean_debug_print ("Audio notify: Cannot beep, no display found\n"); + } +#endif + + print_debug ("ReminderWatcher::Notify Audio for '%s'", reminder_watcher_get_rd_summary (rd)); + + return FALSE; +} + +static gchar * +reminder_watcher_build_notif_id (const EReminderData *rd) +{ + GString *string; + ECalComponentId *id; + ECalComponentAlarmInstance *instance; + + g_return_val_if_fail (rd != NULL, NULL); + + string = g_string_sized_new (32); + + if (e_reminder_data_get_source_uid (rd)) + { + g_string_append (string, e_reminder_data_get_source_uid (rd)); + g_string_append_c (string, '\n'); + } + + id = e_cal_component_get_id (e_reminder_data_get_component (rd)); + if (id) + { + if (e_cal_component_id_get_uid (id)) + { + g_string_append (string, e_cal_component_id_get_uid (id)); + g_string_append_c (string, '\n'); + } + + if (e_cal_component_id_get_rid (id)) + { + g_string_append (string, e_cal_component_id_get_rid (id)); + g_string_append_c (string, '\n'); + } + + e_cal_component_id_free (id); + } + + instance = e_reminder_data_get_instance (rd); + + g_string_append_printf (string, "%" G_GINT64_FORMAT, (gint64) (instance ? e_cal_component_alarm_instance_get_time (instance) : -1)); + + return g_string_free (string, FALSE); +} + +static gboolean +reminder_watcher_notify_display (ReminderWatcher *rw, + const EReminderData *rd, + ECalComponentAlarm *alarm) +{ + GNotification *notification; +#if 0 + GtkIconInfo *icon_info; +#endif + gchar *description, *notif_id; + + g_return_val_if_fail (rw != NULL, FALSE); + g_return_val_if_fail (rd != NULL, FALSE); + g_return_val_if_fail (alarm != NULL, FALSE); + + notif_id = reminder_watcher_build_notif_id (rd); + description = e_reminder_watcher_describe_data (E_REMINDER_WATCHER (rw), rd, E_REMINDER_WATCHER_DESCRIBE_FLAG_NONE); + + notification = g_notification_new (_("Reminders")); + g_notification_set_body (notification, description); + +#if 0 + icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (), "appointment-soon", GTK_ICON_SIZE_DIALOG, 0); + if (icon_info) + { + const gchar *filename; + + filename = gtk_icon_info_get_filename (icon_info); + if (filename && *filename) + { + GFile *file; + GIcon *icon; + + file = g_file_new_for_path (filename); + icon = g_file_icon_new (file); + + if (icon) + { + g_notification_set_icon (notification, icon); + g_object_unref (icon); + } + + g_object_unref (file); + } + + gtk_icon_info_free (icon_info); + } +#endif + + g_application_send_notification (rw->priv->application, notif_id, notification); + + g_object_unref (notification); + g_free (description); + g_free (notif_id); + + print_debug ("ReminderWatcher::Notify Display for '%s'", reminder_watcher_get_rd_summary (rd)); + + return TRUE; +} + +static gboolean +reminder_watcher_notify_email (ReminderWatcher *rw, + const EReminderData *rd, + ECalComponentAlarm *alarm) +{ + print_debug ("ReminderWatcher::Notify Email for '%s'", reminder_watcher_get_rd_summary (rd)); + + /* Nothing to do here */ + return FALSE; +} + +static gboolean +reminder_watcher_is_blessed_program (GSettings *settings, + const gchar *url) +{ + gchar **list; + gint ii; + gboolean found = FALSE; + + g_return_val_if_fail (G_IS_SETTINGS (settings), FALSE); + g_return_val_if_fail (url != NULL, FALSE); + + list = g_settings_get_strv (settings, "notify-programs"); + + for (ii = 0; list && list[ii] && !found; ii++) + { + found = g_strcmp0 (list[ii], url) == 0; + } + + g_strfreev (list); + + return found; +} + +#if 0 +static void +reminder_watcher_save_blessed_program (GSettings *settings, + const gchar *url) +{ + gchar **list; + gint ii; + GPtrArray *array; + + g_return_if_fail (G_IS_SETTINGS (settings)); + g_return_if_fail (url != NULL); + + array = g_ptr_array_new (); + + list = g_settings_get_strv (settings, "notify-programs"); + + for (ii = 0; list && list[ii]; ii++) + { + if (g_strcmp0 (url, list[ii]) != 0) + g_ptr_array_add (array, list[ii]); + } + + g_ptr_array_add (array, (gpointer) url); + g_ptr_array_add (array, NULL); + + g_settings_set_strv (settings, "notify-programs", (const gchar * const *) array->pdata); + + g_ptr_array_free (array, TRUE); + g_strfreev (list); +} +#endif + +static gboolean +reminder_watcher_can_procedure (ReminderWatcher *rw, + const gchar *cmd, + const gchar *url) +{ +#if 0 + GtkWidget *container; + GtkWidget *dialog; + GtkWidget *label; + GtkWidget *checkbox; + gchar *str; + gint response; +#endif + + if (reminder_watcher_is_blessed_program (rw->priv->settings, url)) + return TRUE; + +#if 0 + dialog = gtk_dialog_new_with_buttons ( + _("Warning"), GTK_WINDOW (rw->priv->window), 0, + _("_No"), GTK_RESPONSE_CANCEL, + _("_Yes"), GTK_RESPONSE_OK, + NULL); + + str = g_strdup_printf ( + _("A 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); + + response = gtk_dialog_run (GTK_DIALOG (dialog)); + + if (response == GTK_RESPONSE_OK && + gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (checkbox))) + { + reminder_watcher_save_blessed_program (rw->priv->settings, url); + } + + gtk_widget_destroy (dialog); + + return response == GTK_RESPONSE_OK; +#endif + + return FALSE; +} + +static gboolean +reminder_watcher_notify_procedure (ReminderWatcher *rw, + const EReminderData *rd, + ECalComponentAlarm *alarm) +{ + ECalComponentText *description; + ICalAttach *attach = NULL; + GSList *attachments; + const gchar *url; + gchar *cmd; + gboolean result = FALSE; + + g_return_val_if_fail (rw != NULL, FALSE); + g_return_val_if_fail (rd != NULL, FALSE); + g_return_val_if_fail (alarm != NULL, FALSE); + + print_debug ("ReminderWatcher::Notify Procedure for '%s'", reminder_watcher_get_rd_summary (rd)); + + attachments = e_cal_component_alarm_get_attachments (alarm); + + if (attachments && !attachments->next) + attach = attachments->data; + + description = e_cal_component_alarm_get_description (alarm); + + /* If the alarm has no attachment, simply display a notification dialog. */ + if (!attach) + goto fallback; + + if (!i_cal_attach_get_is_url (attach)) + goto fallback; + + url = i_cal_attach_get_url (attach); + g_return_val_if_fail (url != NULL, FALSE); + + /* Ask for confirmation before executing the stuff */ + if (description && e_cal_component_text_get_value (description)) + cmd = g_strconcat (url, " ", e_cal_component_text_get_value (description), NULL); + else + cmd = (gchar *) url; + + if (reminder_watcher_can_procedure (rw, cmd, url)) + result = g_spawn_command_line_async (cmd, NULL); + + if (cmd != (gchar *) url) + g_free (cmd); + + /* Fall back to display notification if we got an error */ + if (!result) + goto fallback; + + return FALSE; + + fallback: + + return reminder_watcher_notify_display (rw, rd, alarm); +} + +/* Returns %TRUE to keep it, %FALSE to dismiss it */ +static gboolean +reminders_process_one (ReminderWatcher *rw, + const EReminderData *rd, + gboolean snoozed) +{ + ECalComponentAlarm *alarm; + ECalComponentAlarmInstance *instance; + ECalComponentAlarmAction action; + gboolean keep_in_reminders = FALSE; + + g_return_val_if_fail (rw != NULL, FALSE); + g_return_val_if_fail (rd != NULL, FALSE); + + if (e_cal_component_get_vtype (e_reminder_data_get_component (rd)) == E_CAL_COMPONENT_TODO) + { + ICalPropertyStatus status; + + status = e_cal_component_get_status (e_reminder_data_get_component (rd)); + + if (status == I_CAL_STATUS_COMPLETED && + !g_settings_get_boolean (rw->priv->settings, "notify-completed-tasks")) + return FALSE; + } + + instance = e_reminder_data_get_instance (rd); + + alarm = instance ? e_cal_component_get_alarm (e_reminder_data_get_component (rd), e_cal_component_alarm_instance_get_uid (instance)) : NULL; + if (!alarm) + return FALSE; + + if (!snoozed && !g_settings_get_boolean (rw->priv->settings, "notify-past-events")) + { + ECalComponentAlarmTrigger *trigger; + time_t offset = 0, event_relative, orig_trigger_day, today; + + trigger = e_cal_component_alarm_get_trigger (alarm); + + switch (trigger ? e_cal_component_alarm_trigger_get_kind (trigger) : E_CAL_COMPONENT_ALARM_TRIGGER_NONE) + { + case E_CAL_COMPONENT_ALARM_TRIGGER_NONE: + case E_CAL_COMPONENT_ALARM_TRIGGER_ABSOLUTE: + break; + + case E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START: + case E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_END: + offset = i_cal_duration_as_int (e_cal_component_alarm_trigger_get_duration (trigger)); + break; + + default: + break; + } + + today = time (NULL); + event_relative = e_cal_component_alarm_instance_get_occur_start (instance) - offset; + + #define CLAMP_TO_DAY(x) ((x) - ((x) % (60 * 60 * 24))) + + event_relative = CLAMP_TO_DAY (event_relative); + orig_trigger_day = CLAMP_TO_DAY (e_cal_component_alarm_instance_get_time (instance)); + today = CLAMP_TO_DAY (today); + + #undef CLAMP_TO_DAY + + if (event_relative < today && orig_trigger_day < today) + { + e_cal_component_alarm_free (alarm); + return FALSE; + } + } + + action = e_cal_component_alarm_get_action (alarm); + + switch (action) + { + case E_CAL_COMPONENT_ALARM_AUDIO: + keep_in_reminders = reminder_watcher_notify_audio (rw, rd, alarm); + break; + + case E_CAL_COMPONENT_ALARM_DISPLAY: + keep_in_reminders = reminder_watcher_notify_display (rw, rd, alarm); + break; + + case E_CAL_COMPONENT_ALARM_EMAIL: + keep_in_reminders = reminder_watcher_notify_email (rw, rd, alarm); + break; + + case E_CAL_COMPONENT_ALARM_PROCEDURE: + keep_in_reminders = reminder_watcher_notify_procedure (rw, rd, alarm); + break; + + case E_CAL_COMPONENT_ALARM_NONE: + case E_CAL_COMPONENT_ALARM_UNKNOWN: + break; + } + + e_cal_component_alarm_free (alarm); + + return keep_in_reminders; +} + +static gpointer +reminders_dismiss_thread (gpointer user_data) +{ + ReminderWatcher *rw = user_data; + EReminderWatcher *watcher; + GSList *dismiss, *link; + + g_return_val_if_fail (IS_REMINDER_WATCHER (rw), NULL); + + g_mutex_lock (&rw->priv->dismiss_lock); + dismiss = rw->priv->dismiss; + rw->priv->dismiss = NULL; + rw->priv->dismiss_thread = NULL; + g_mutex_unlock (&rw->priv->dismiss_lock); + + watcher = E_REMINDER_WATCHER (rw); + if (watcher) + { + for (link = dismiss; link; link = g_slist_next (link)) + { + EReminderData *rd = link->data; + + if (rd) + { + /* Silently ignore any errors here */ + e_reminder_watcher_dismiss_sync (watcher, rd, NULL, NULL); + } + } + } + + g_slist_free_full (dismiss, e_reminder_data_free); + g_clear_object (&rw); + + return NULL; +} + +static void +reminders_triggered_cb (EReminderWatcher *watcher, + const GSList *reminders, /* EReminderData * */ + gboolean snoozed, + gpointer user_data) +{ + ReminderWatcher *rw = REMINDER_WATCHER (watcher); + GSList *link; + + g_return_if_fail (IS_REMINDER_WATCHER (rw)); + + g_mutex_lock (&rw->priv->dismiss_lock); + + for (link = (GSList *) reminders; link; link = g_slist_next (link)) + { + const EReminderData *rd = link->data; + + if (rd && !reminders_process_one (rw, rd, snoozed)) + { + rw->priv->dismiss = g_slist_prepend (rw->priv->dismiss, e_reminder_data_copy (rd)); + } + } + + if (rw->priv->dismiss && !rw->priv->dismiss_thread) + { + rw->priv->dismiss_thread = g_thread_new (NULL, reminders_dismiss_thread, g_object_ref (rw)); + g_warn_if_fail (rw->priv->dismiss_thread != NULL); + if (rw->priv->dismiss_thread) + g_thread_unref (rw->priv->dismiss_thread); + } + + g_mutex_unlock (&rw->priv->dismiss_lock); +} + +static EClient * +reminder_watcher_cal_client_connect_sync (EReminderWatcher *watcher, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GError **error) +{ + ReminderWatcher *reminder_watcher = REMINDER_WATCHER (watcher); + + return calendar_sources_connect_client_sync (reminder_watcher->priv->sources, FALSE, source, source_type, + wait_for_connected_seconds, cancellable, error); +} + +static void +reminder_watcher_cal_client_connect (EReminderWatcher *watcher, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ReminderWatcher *reminder_watcher = REMINDER_WATCHER (watcher); + + calendar_sources_connect_client (reminder_watcher->priv->sources, FALSE, source, source_type, + wait_for_connected_seconds, cancellable, callback, user_data); +} + +static EClient * +reminder_watcher_cal_client_connect_finish (EReminderWatcher *watcher, + GAsyncResult *result, + GError **error) +{ + ReminderWatcher *reminder_watcher = REMINDER_WATCHER (watcher); + + return calendar_sources_connect_client_finish (reminder_watcher->priv->sources, result, error); +} + +static void +reminder_watcher_constructed (GObject *object) +{ + ReminderWatcher *rw = REMINDER_WATCHER (object); + + G_OBJECT_CLASS (reminder_watcher_parent_class)->constructed (object); + + rw->priv->sources = calendar_sources_get (); + + g_signal_connect (rw, "triggered", + G_CALLBACK (reminders_triggered_cb), NULL); +} + +static void +reminder_watcher_finalize (GObject *object) +{ + ReminderWatcher *rw = REMINDER_WATCHER (object); + + g_clear_object (&rw->priv->sources); + g_clear_object (&rw->priv->settings); + g_mutex_clear (&rw->priv->dismiss_lock); + + G_OBJECT_CLASS (reminder_watcher_parent_class)->finalize (object); +} + +static void +reminder_watcher_class_init (ReminderWatcherClass *klass) +{ + GObjectClass *object_class; + EReminderWatcherClass *watcher_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->constructed = reminder_watcher_constructed; + object_class->finalize = reminder_watcher_finalize; + + watcher_class = E_REMINDER_WATCHER_CLASS (klass); + watcher_class->cal_client_connect_sync = reminder_watcher_cal_client_connect_sync; + watcher_class->cal_client_connect = reminder_watcher_cal_client_connect; + watcher_class->cal_client_connect_finish = reminder_watcher_cal_client_connect_finish; +} + +static void +reminder_watcher_init (ReminderWatcher *rw) +{ + rw->priv = reminder_watcher_get_instance_private (rw); + rw->priv->settings = g_settings_new ("org.gnome.evolution-data-server.calendar"); + + g_mutex_init (&rw->priv->dismiss_lock); +} + +EReminderWatcher * +reminder_watcher_new (GApplication *application, + ESourceRegistry *registry) +{ + ReminderWatcher *rw; + + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL); + + rw = g_object_new (TYPE_REMINDER_WATCHER, + "registry", registry, + NULL); + + rw->priv->application = application; + + return E_REMINDER_WATCHER (rw); +} diff --git a/src/calendar-server/reminder-watcher.h b/src/calendar-server/reminder-watcher.h new file mode 100644 index 000000000..f98fadf77 --- /dev/null +++ b/src/calendar-server/reminder-watcher.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 Red Hat (www.redhat.com) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#ifndef REMINDER_WATCHER_H +#define REMINDER_WATCHER_H + +#define HANDLE_LIBICAL_MEMORY +#define EDS_DISABLE_DEPRECATED +#include + +/* Standard GObject macros */ +#define TYPE_REMINDER_WATCHER \ + (reminder_watcher_get_type ()) +#define REMINDER_WATCHER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), TYPE_REMINDER_WATCHER, ReminderWatcher)) +#define REMINDER_WATCHER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), TYPE_REMINDER_WATCHER, ReminderWatcherClass)) +#define IS_REMINDER_WATCHER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), TYPE_REMINDER_WATCHER)) +#define IS_REMINDER_WATCHER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), TYPE_REMINDER_WATCHER)) +#define REMINDER_WATCHER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), TYPE_REMINDER_WATCHER, ReminderWatcherClass)) + +G_BEGIN_DECLS + +typedef struct _ReminderWatcher ReminderWatcher; +typedef struct _ReminderWatcherClass ReminderWatcherClass; +typedef struct _ReminderWatcherPrivate ReminderWatcherPrivate; + +struct _ReminderWatcher { + EReminderWatcher parent; + ReminderWatcherPrivate *priv; +}; + +struct _ReminderWatcherClass { + EReminderWatcherClass parent_class; +}; + +GType reminder_watcher_get_type (void) G_GNUC_CONST; +EReminderWatcher *reminder_watcher_new (GApplication *application, + ESourceRegistry *registry); + +G_END_DECLS + +#endif /* REMINDER_WATCHER_H */