Files
federated-calendar/src/core/gcal-calendar-monitor.c
T
John Cardullo 9d29a7a34e core: Add "expand-recurrences" property to GcalCalendarMonitor and GcalTimeline
Add an "expand-recurrences" boolean property (defaulting to TRUE) to both
GcalCalendarMonitor and GcalTimeline. When set to FALSE:
- Recurring master components are kept as single events without expanding
  each recurrence instance.
- Detached recurrence instances are ignored.
- Pruning outside range retains recurring master events so they are not
  prematurely removed.
- Subscriber filtering permits recurring master events within the
  subscriber model.

This allows consumers like search providers to avoid expanding all recurrence
instances when searching, improving performance and avoiding duplicate hits.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-calendar/-/merge_requests/808>
2026-10-27 11:10:58 -07:00

1555 lines
45 KiB
C

/* gcal-calendar-monitor.c
*
* Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.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 3 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 <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#define G_LOG_DOMAIN "GcalCalendarMonitor"
#include "gcal-calendar-monitor.h"
#include "gcal-core-macros.h"
#include "gcal-date-time-utils.h"
#include "gcal-debug.h"
#include "gcal-event.h"
#include "gcal-event-list.h"
#include <gio/gio.h>
#include <libecal/libecal.h>
typedef struct
{
GcalCalendarMonitor *monitor;
GPtrArray *events;
GPtrArray *event_ids;
gboolean complete;
} IdleData;
typedef enum
{
INVALID_EVENT,
CREATE_VIEW,
REMOVE_VIEW,
RANGE_UPDATED,
FILTER_UPDATED,
QUIT,
} MonitorThreadEvent;
struct _GcalCalendarMonitor
{
GObject parent;
GThread *thread;
GCancellable *cancellable;
GMainContext *thread_context;
GMainContext *main_context;
GAsyncQueue *messages;
GcalCalendar *calendar;
gboolean complete;
GcalEventList *event_list;
/*
* These fields are only accessed on the monitor thread, and
* never on the main thread.
*/
struct {
gboolean populated;
GPtrArray *events_to_add;
ECalClientView *view;
} monitor_thread;
/*
* Accessing any of the fields below must be happen inside
* a locked muxed.
*/
struct {
GRWLock lock;
GHashTable *events; /* gchar* -> GcalEvent* */
GcalRange *range;
gchar *filter;
gboolean expand_recurrences;
} shared;
};
static gboolean add_events_to_timeline_in_idle_cb (gpointer user_data);
static gboolean update_events_in_idle_cb (gpointer user_data);
static gboolean remove_events_from_timeline_in_idle_cb (gpointer user_data);
static gboolean complete_in_idle_cb (gpointer user_data);
static void g_list_model_interface_init (GListModelInterface *iface);
G_DEFINE_TYPE_WITH_CODE (GcalCalendarMonitor, gcal_calendar_monitor, G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, g_list_model_interface_init))
enum
{
PROP_0,
PROP_CALENDAR,
PROP_COMPLETE,
PROP_EXPAND_RECURRENCES,
N_PROPS
};
static GParamSpec *properties [N_PROPS] = { NULL, };
/*
* GListModel interface
*/
static GType
gcal_calendar_monitor_get_item_type (GListModel *model G_GNUC_UNUSED)
{
return GCAL_TYPE_EVENT;
}
static guint
gcal_calendar_monitor_get_n_items (GListModel *model)
{
GcalCalendarMonitor *self = (GcalCalendarMonitor *) model;
g_assert (GCAL_IS_CALENDAR_MONITOR (self));
return g_list_model_get_n_items (G_LIST_MODEL (self->event_list));
}
static gpointer
gcal_calendar_monitor_get_item (GListModel *model,
guint position)
{
GcalCalendarMonitor *self = (GcalCalendarMonitor *) model;
g_assert (GCAL_IS_CALENDAR_MONITOR (self));
return g_list_model_get_item (G_LIST_MODEL (self->event_list), position);
}
static void
g_list_model_interface_init (GListModelInterface *iface)
{
iface->get_item_type = gcal_calendar_monitor_get_item_type;
iface->get_n_items = gcal_calendar_monitor_get_n_items;
iface->get_item = gcal_calendar_monitor_get_item;
}
/*
* Threads
*
* These methods must *never* be executed in the main thread.
*/
static gchar*
build_subscriber_filter (GcalRange *range,
const gchar *filter)
{
g_autoptr (GDateTime) inclusive_range_end = NULL;
g_autoptr (GDateTime) utc_range_start = NULL;
g_autoptr (GDateTime) utc_range_end = NULL;
g_autoptr (GDateTime) range_start = NULL;
g_autoptr (GDateTime) range_end = NULL;
g_autofree gchar *start_str = NULL;
g_autofree gchar *end_str = NULL;
g_autofree gchar *result = NULL;
/*
* XXX: E-D-S ISO8601 parser is incomplete and doesn't accept the output of
* g_date_time_format_iso8601().
*/
range_start = gcal_range_get_start (range);
utc_range_start = g_date_time_to_utc (range_start);
start_str = g_date_time_format (utc_range_start, "%Y%m%dT%H%M%SZ");
range_end = gcal_range_get_end (range);
inclusive_range_end = g_date_time_add_seconds (range_end, -1);
utc_range_end = g_date_time_to_utc (inclusive_range_end);
end_str = g_date_time_format (utc_range_end, "%Y%m%dT%H%M%SZ");
if (filter)
{
result = g_strdup_printf ("(and (occur-in-time-range? (make-time \"%s\") (make-time \"%s\")) %s)",
start_str,
end_str,
filter);
}
else
{
result = g_strdup_printf ("(occur-in-time-range? (make-time \"%s\") (make-time \"%s\"))",
start_str,
end_str);
}
return g_steal_pointer (&result);
}
static void
maybe_init_event_arrays (GcalCalendarMonitor *self)
{
if (self->monitor_thread.populated)
return;
if (!self->monitor_thread.events_to_add)
self->monitor_thread.events_to_add = g_ptr_array_new_with_free_func (g_object_unref);
}
static GcalRange*
get_monitor_ranges (GcalCalendarMonitor *self)
{
g_autoptr (GcalRange) range = NULL;
g_rw_lock_reader_lock (&self->shared.lock);
range = gcal_range_copy (self->shared.range);
g_rw_lock_reader_unlock (&self->shared.lock);
return g_steal_pointer (&range);
}
static void
idle_data_free (IdleData *data)
{
g_clear_object (&data->monitor);
g_clear_pointer (&data->events, g_ptr_array_unref);
g_clear_pointer (&data->event_ids, g_ptr_array_unref);
g_free (data);
}
static void
add_events_in_idle (GcalCalendarMonitor *self,
GPtrArray *events)
{
IdleData *idle_data;
g_assert (GCAL_IS_THREAD (self->thread));
idle_data = g_new0 (IdleData, 1);
idle_data->monitor = g_object_ref (self);
idle_data->events = g_ptr_array_ref (events);
g_main_context_invoke_full (self->main_context,
G_PRIORITY_DEFAULT_IDLE,
add_events_to_timeline_in_idle_cb,
idle_data,
(GDestroyNotify) idle_data_free);
}
static void
update_events_in_idle (GcalCalendarMonitor *self,
GPtrArray *events)
{
IdleData *idle_data;
g_assert (GCAL_IS_THREAD (self->thread));
idle_data = g_new0 (IdleData, 1);
idle_data->monitor = g_object_ref (self);
idle_data->events = g_ptr_array_ref (events);
g_main_context_invoke_full (self->main_context,
G_PRIORITY_DEFAULT_IDLE,
update_events_in_idle_cb,
idle_data,
(GDestroyNotify) idle_data_free);
}
static void
remove_events_in_idle (GcalCalendarMonitor *self,
GPtrArray *events)
{
IdleData *idle_data;
g_assert (GCAL_IS_THREAD (self->thread));
idle_data = g_new0 (IdleData, 1);
idle_data->monitor = g_object_ref (self);
idle_data->event_ids = g_ptr_array_ref (events);
g_main_context_invoke_full (self->main_context,
G_PRIORITY_DEFAULT_IDLE,
remove_events_from_timeline_in_idle_cb,
idle_data,
(GDestroyNotify) idle_data_free);
}
static void
set_complete_in_idle (GcalCalendarMonitor *self,
gboolean complete)
{
IdleData *idle_data;
g_assert (GCAL_IS_THREAD (self->thread));
idle_data = g_new0 (IdleData, 1);
idle_data->monitor = g_object_ref (self);
idle_data->complete = complete;
g_main_context_invoke_full (self->main_context,
G_PRIORITY_DEFAULT_IDLE,
complete_in_idle_cb,
idle_data,
(GDestroyNotify) idle_data_free);
}
typedef struct
{
GcalCalendarMonitor *monitor;
GPtrArray *expanded_events;
} GenerateRecurrencesData;
static gboolean
client_instance_generated_cb (ICalComponent *icomponent,
ICalTime *instance_start,
ICalTime *instance_end,
gpointer user_data,
GCancellable *cancellable,
GError **error)
{
g_autoptr (GcalEvent) event = NULL;
g_autoptr (GError) local_error = NULL;
GenerateRecurrencesData *data;
GcalCalendarMonitor *self;
ECalComponent *ecomponent;
data = user_data;
self = data->monitor;
g_assert (GCAL_IS_THREAD (self->thread));
if (g_cancellable_set_error_if_cancelled (cancellable, error))
return FALSE;
ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent));
if (!ecomponent)
return TRUE;
event = gcal_event_new (self->calendar, ecomponent, &local_error);
g_clear_object (&ecomponent);
if (local_error)
{
g_propagate_error (error, local_error);
return TRUE;
}
g_ptr_array_add (data->expanded_events, g_steal_pointer (&event));
return TRUE;
}
static void
on_client_view_objects_added_cb (ECalClientView *view,
const GSList *objects,
GcalCalendarMonitor *self)
{
g_autoptr (GPtrArray) components_to_expand = NULL;
g_autoptr (GPtrArray) events_to_add = NULL;
g_autoptr (GcalRange) range = NULL;
const GSList *l;
gint i;
GCAL_ENTRY;
g_assert (GCAL_IS_THREAD (self->thread));
range = get_monitor_ranges (self);
maybe_init_event_arrays (self);
components_to_expand = g_ptr_array_new ();
events_to_add = g_ptr_array_new_with_free_func (g_object_unref);
for (l = objects; l; l = l->next)
{
g_autoptr (GcalEvent) event = NULL;
g_autoptr (GError) error = NULL;
ICalComponent *icomponent;
ECalComponent *ecomponent;
icomponent = l->data;
if (g_cancellable_is_cancelled (self->cancellable))
return;
if (!icomponent || !i_cal_component_get_uid (icomponent))
continue;
if (self->shared.expand_recurrences)
{
/* Recurrent events will be processed later */
if (e_cal_util_component_has_recurrences (icomponent) &&
!e_cal_util_component_is_instance (icomponent))
{
GCAL_TRACE_MSG ("Component %s (%s) needs to be expanded",
i_cal_component_get_summary (icomponent),
i_cal_component_get_uid (icomponent));
g_ptr_array_add (components_to_expand, icomponent);
continue;
}
}
else if (e_cal_util_component_is_instance (icomponent))
{
/*
* When recurrence expansion is disabled, ignore detached recurrence
* instances (components with a RECURRENCE-ID) returned by EDS so
* only the master recurring component is processed.
*/
continue;
}
ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent));
if (!ecomponent)
continue;
event = gcal_event_new (self->calendar, ecomponent, &error);
g_clear_object (&ecomponent);
if (error)
{
g_warning ("Error creating event: %s", error->message);
continue;
}
if (!self->monitor_thread.populated)
g_ptr_array_add (self->monitor_thread.events_to_add, g_object_ref (event));
else
g_ptr_array_add (events_to_add, g_object_ref (event));
}
/* Recurrent events */
if (components_to_expand->len > 0)
{
g_autoptr (GDateTime) range_start = NULL;
g_autoptr (GDateTime) range_end = NULL;
ECalClient *client;
time_t range_start_time;
time_t range_end_time;
GCAL_TRACE_MSG ("Expanding recurrencies of %d events", components_to_expand->len);
client = gcal_calendar_get_client (self->calendar);
range_start = gcal_range_get_start (range);
range_end = gcal_range_get_end (range);
range_start_time = g_date_time_to_unix (range_start);
range_end_time = g_date_time_to_unix (range_end) - 1;
/* Generate the instances */
for (i = 0; i < components_to_expand->len; i++)
{
GenerateRecurrencesData recurrences_data;
ICalComponent *icomponent;
#if GCAL_ENABLE_TRACE
gint old_size;
#endif
if (g_cancellable_is_cancelled (self->cancellable))
return;
icomponent = g_ptr_array_index (components_to_expand, i);
recurrences_data.monitor = self;
if (!self->monitor_thread.populated)
recurrences_data.expanded_events = self->monitor_thread.events_to_add;
else
recurrences_data.expanded_events = events_to_add;
#if GCAL_ENABLE_TRACE
old_size = recurrences_data.expanded_events->len;
#endif
e_cal_client_generate_instances_for_object_sync (client,
icomponent,
range_start_time,
range_end_time,
self->cancellable,
client_instance_generated_cb,
&recurrences_data);
#if GCAL_ENABLE_TRACE
{
g_autofree gchar *range_str = gcal_range_to_string (range);
GCAL_TRACE_MSG ("Component %s (%s) added %d instance(s) between %s",
i_cal_component_get_summary (icomponent),
i_cal_component_get_uid (icomponent),
recurrences_data.expanded_events->len - old_size,
range_str);
}
#endif
}
}
if (events_to_add->len > 0)
add_events_in_idle (self, events_to_add);
GCAL_EXIT;
}
static void
on_client_view_objects_modified_cb (ECalClientView *view,
const GSList *objects,
GcalCalendarMonitor *self)
{
g_autoptr (GHashTable) events_to_remove = NULL;
g_autoptr (GPtrArray) components_to_expand = NULL;
g_autoptr (GPtrArray) event_ids_to_remove = NULL;
g_autoptr (GPtrArray) events_to_update = NULL;
g_autoptr (GcalRange) range = NULL;
const GSList *l;
GCAL_ENTRY;
g_assert (GCAL_IS_THREAD (self->thread));
if (!self->monitor_thread.populated && self->monitor_thread.events_to_add)
{
g_clear_pointer (&self->monitor_thread.events_to_add, g_ptr_array_unref);
return;
}
G_RW_LOCK_READER_AUTO_LOCK (&self->shared.lock, reader_locker);
components_to_expand = g_ptr_array_new ();
events_to_remove = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
events_to_update = g_ptr_array_new_with_free_func (g_object_unref);
range = gcal_range_copy (self->shared.range);
for (l = objects; l; l = l->next)
{
g_autoptr (GcalEvent) event = NULL;
g_autoptr (GError) error = NULL;
g_autofree gchar *event_id = NULL;
ECalComponentId *component_id;
ICalComponent *icomponent;
ECalComponent *ecomponent;
gboolean recurrence_main;
icomponent = l->data;
if (!icomponent || !i_cal_component_get_uid (icomponent))
continue;
/* Ignore modified detached recurrence instances when expansion is disabled */
if (!self->shared.expand_recurrences && e_cal_util_component_is_instance (icomponent))
continue;
ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent));
if (!ecomponent)
continue;
recurrence_main = self->shared.expand_recurrences &&
e_cal_util_component_has_recurrences (icomponent) &&
!e_cal_util_component_is_instance (icomponent);
/*
* If the event has no recurrence id, it is either a main event, or a
* regular non-recurring event. If we're receiving the 'objects-modified'
* signal because this event went from recurring to non-recurring, the
* instances of the old recurrency are still here. They need to be removed.
*/
component_id = e_cal_component_get_id (ecomponent);
event_id = g_strdup_printf ("%s:%s",
gcal_calendar_get_id (self->calendar),
e_cal_component_id_get_uid (component_id));
if (!e_cal_component_id_get_rid (component_id))
{
GHashTableIter iter;
const gchar *aux;
g_hash_table_iter_init (&iter, self->shared.events);
while (g_hash_table_iter_next (&iter, (gpointer*) &aux, NULL))
{
if (!g_str_has_prefix (aux, event_id))
continue;
if (recurrence_main || !g_str_equal (aux, event_id))
g_hash_table_add (events_to_remove, g_strdup (aux));
}
}
g_clear_pointer (&component_id, e_cal_component_id_free);
/* Recurrent events will be processed later */
if (recurrence_main)
{
GCAL_TRACE_MSG ("Component %s (%s) needs to be expanded",
i_cal_component_get_summary (icomponent),
i_cal_component_get_uid (icomponent));
g_ptr_array_add (components_to_expand, icomponent);
continue;
}
event = gcal_event_new (self->calendar, ecomponent, &error);
g_clear_object (&ecomponent);
if (error)
{
g_warning ("Error creating event: %s", error->message);
continue;
}
g_ptr_array_add (events_to_update, g_steal_pointer (&event));
}
/* Recurrent events */
if (components_to_expand->len > 0)
{
g_autoptr (GPtrArray) expanded_events = NULL;
g_autoptr (GPtrArray) events_to_add = NULL;
g_autoptr (GDateTime) range_start = NULL;
g_autoptr (GDateTime) range_end = NULL;
ECalClient *client;
time_t range_start_time;
time_t range_end_time;
GCAL_TRACE_MSG ("Expanding recurrencies of %d events", components_to_expand->len);
client = gcal_calendar_get_client (self->calendar);
range_start = gcal_range_get_start (range);
range_end = gcal_range_get_end (range);
range_start_time = g_date_time_to_unix (range_start);
range_end_time = g_date_time_to_unix (range_end) - 1;
expanded_events = g_ptr_array_new_with_free_func (g_object_unref);
events_to_add = g_ptr_array_new_with_free_func (g_object_unref);
/* Generate the instances */
for (guint i = 0; i < components_to_expand->len; i++)
{
GenerateRecurrencesData recurrences_data;
ICalComponent *icomponent;
#if GCAL_ENABLE_TRACE
gint old_size;
#endif
if (g_cancellable_is_cancelled (self->cancellable))
return;
icomponent = g_ptr_array_index (components_to_expand, i);
recurrences_data.monitor = self;
recurrences_data.expanded_events = expanded_events;
#if GCAL_ENABLE_TRACE
old_size = expanded_events->len;
#endif
e_cal_client_generate_instances_for_object_sync (client,
icomponent,
range_start_time,
range_end_time,
self->cancellable,
client_instance_generated_cb,
&recurrences_data);
#if GCAL_ENABLE_TRACE
{
g_autofree gchar *range_str = gcal_range_to_string (range);
GCAL_TRACE_MSG ("Component %s (%s) added %d instance(s) between %s",
i_cal_component_get_summary (icomponent),
i_cal_component_get_uid (icomponent),
expanded_events->len - old_size,
range_str);
}
#endif
}
for (guint i = 0; i < expanded_events->len; i++)
{
GcalEvent *event = g_ptr_array_index (expanded_events, i);
GcalEvent *old_event;
if (g_cancellable_is_cancelled (self->cancellable))
return;
old_event = g_hash_table_lookup (self->shared.events, gcal_event_get_uid (event));
if (old_event)
{
g_hash_table_remove (events_to_remove, gcal_event_get_uid (event));
g_ptr_array_add (events_to_update, g_object_ref (event));
}
else
{
g_ptr_array_add (events_to_add, g_object_ref (event));
}
}
if (events_to_add->len > 0)
add_events_in_idle (self, events_to_add);
}
if (events_to_update->len > 0)
update_events_in_idle (self, events_to_update);
/* Now remove lingering events */
event_ids_to_remove = g_hash_table_steal_all_keys (events_to_remove);
if (event_ids_to_remove->len > 0)
remove_events_in_idle (self, event_ids_to_remove);
GCAL_EXIT;
}
static void
on_client_view_objects_removed_cb (ECalClientView *view,
const GSList *objects,
GcalCalendarMonitor *self)
{
g_autoptr (GPtrArray) event_ids = NULL;
const GSList *l;
GCAL_ENTRY;
g_assert (GCAL_IS_THREAD (self->thread));
event_ids = g_ptr_array_new_with_free_func (g_free);
for (l = objects; l; l = l->next)
{
g_autofree gchar *event_id = NULL;
ECalComponentId *component_id;
if (g_cancellable_is_cancelled (self->cancellable))
return;
component_id = l->data;
if (e_cal_component_id_get_rid (component_id))
{
event_id = g_strdup_printf ("%s:%s:%s",
gcal_calendar_get_id (self->calendar),
e_cal_component_id_get_uid (component_id),
e_cal_component_id_get_rid (component_id));
}
else
{
GHashTableIter iter;
const gchar *aux;
event_id = g_strdup_printf ("%s:%s",
gcal_calendar_get_id (self->calendar),
e_cal_component_id_get_uid (component_id));
/*
* If this is the main component, remove the expanded recurrency instances
* as well.
*/
G_RW_LOCK_READER_AUTO_LOCK (&self->shared.lock, reader_locker);
g_hash_table_iter_init (&iter, self->shared.events);
while (g_hash_table_iter_next (&iter, (gpointer*) &aux, NULL))
{
if (!g_str_equal (aux, event_id) && g_str_has_prefix (aux, event_id))
g_ptr_array_add (event_ids, g_strdup (aux));
}
}
g_ptr_array_add (event_ids, g_strdup (event_id));
}
if (event_ids->len > 0)
remove_events_in_idle (self, event_ids);
GCAL_EXIT;
}
static void
on_client_view_complete_cb (ECalClientView *view,
const GError *error,
GcalCalendarMonitor *self)
{
g_autoptr (GPtrArray) events_to_add = NULL;
GCAL_ENTRY;
g_assert (GCAL_IS_THREAD (self->thread));
g_assert (!self->monitor_thread.populated);
events_to_add = g_steal_pointer (&self->monitor_thread.events_to_add);
if (events_to_add)
add_events_in_idle (self, events_to_add);
self->monitor_thread.populated = TRUE;
set_complete_in_idle (self, TRUE);
g_debug ("Finished initial loading of calendar '%s'", gcal_calendar_get_name (self->calendar));
GCAL_EXIT;
}
static void
create_view (GcalCalendarMonitor *self)
{
g_autoptr (GRWLockReaderLocker) reader_locker = NULL;
g_autofree gchar *filter = NULL;
g_autoptr (GError) error = NULL;
ECalClientView *view;
ECalClient *client;
GCAL_ENTRY;
g_assert (GCAL_IS_THREAD (self->thread));
reader_locker = g_rw_lock_reader_locker_new (&self->shared.lock);
g_assert (self->cancellable == NULL);
self->cancellable = g_cancellable_new ();
if (!self->shared.range)
GCAL_RETURN ();
filter = build_subscriber_filter (self->shared.range, self->shared.filter);
g_clear_pointer (&reader_locker, g_rw_lock_reader_locker_free);
if (!gcal_calendar_get_visible (self->calendar))
GCAL_RETURN ();
client = gcal_calendar_get_client (self->calendar);
e_cal_client_get_view_sync (client, filter, &view, self->cancellable, &error);
if (error)
{
if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
g_warning ("Error creating view: %s", error->message);
GCAL_RETURN ();
}
GCAL_TRACE_MSG ("Initialized ECalClientView with query \"%s\"", filter);
g_signal_connect (view, "objects-added", G_CALLBACK (on_client_view_objects_added_cb), self);
g_signal_connect (view, "objects-modified", G_CALLBACK (on_client_view_objects_modified_cb), self);
g_signal_connect (view, "objects-removed", G_CALLBACK (on_client_view_objects_removed_cb), self);
g_signal_connect (view, "complete", G_CALLBACK (on_client_view_complete_cb), self);
if (g_cancellable_is_cancelled (self->cancellable))
GCAL_RETURN ();
g_debug ("Starting ECalClientView for calendar '%s'", gcal_calendar_get_name (self->calendar));
e_cal_client_view_start (view, &error);
if (error)
{
g_warning ("Error starting up view: %s", error->message);
g_clear_object (&view);
GCAL_RETURN ();
}
self->monitor_thread.view = g_steal_pointer (&view);
set_complete_in_idle (self, FALSE);
GCAL_EXIT;
}
static void
remove_view (GcalCalendarMonitor *self)
{
g_autoptr (GError) error = NULL;
GCAL_ENTRY;
g_assert (GCAL_IS_THREAD (self->thread));
g_clear_object (&self->cancellable);
if (!self->monitor_thread.view)
GCAL_RETURN ();
g_debug ("Tearing down view for calendar %s", gcal_calendar_get_id (self->calendar));
e_cal_client_view_stop (self->monitor_thread.view, &error);
if (error)
{
g_warning ("Error stopping view: %s", error->message);
GCAL_RETURN ();
}
g_clear_object (&self->monitor_thread.view);
self->monitor_thread.populated = FALSE;
GCAL_EXIT;
}
typedef struct
{
GSource parent;
GcalCalendarMonitor *monitor;
GMainLoop *mainloop;
} MessageQueueSource;
static gboolean
message_queue_source_prepare (GSource *source,
gint *timeout)
{
GcalCalendarMonitor *self;
MessageQueueSource *queue_source;
queue_source = (MessageQueueSource*) source;
self = queue_source->monitor;
/*
* The prepare function can be called before g_thread_new()
* returns, and that's valid. However, in that case, no messages
* should be available in the queue, as the main thread didn't
* return execution to the mainloop yet. Protect against that.
*/
g_assert (GCAL_IS_THREAD (self->thread) ||
(!self->thread && g_async_queue_length (self->messages) == 0));
return g_async_queue_length (self->messages) > 0;
}
static gboolean
message_queue_source_dispatch (GSource *source,
GSourceFunc callback,
gpointer user_data)
{
GcalCalendarMonitor *self;
MessageQueueSource *queue_source;
MonitorThreadEvent event;
GCAL_ENTRY;
queue_source = (MessageQueueSource*) source;
self = queue_source->monitor;
event = GPOINTER_TO_INT (g_async_queue_pop (self->messages));
g_assert (GCAL_IS_THREAD (self->thread));
switch (event)
{
case FILTER_UPDATED:
case RANGE_UPDATED:
remove_view (self);
create_view (self);
break;
case CREATE_VIEW:
create_view (self);
break;
case REMOVE_VIEW:
remove_view (self);
break;
case INVALID_EVENT:
case QUIT:
remove_view (self);
g_main_loop_quit (queue_source->mainloop);
GCAL_RETURN (G_SOURCE_REMOVE);
}
GCAL_RETURN (G_SOURCE_CONTINUE);
}
static GSourceFuncs monitor_queue_source_funcs =
{
message_queue_source_prepare,
NULL,
message_queue_source_dispatch,
NULL,
};
static MessageQueueSource*
views_monitor_source_new (GcalCalendarMonitor *self,
GMainLoop *mainloop)
{
MessageQueueSource *queue_source;
GSource *source;
/* Prepare the message queue source */
source = g_source_new (&monitor_queue_source_funcs, sizeof (MessageQueueSource));
queue_source = (MessageQueueSource*) source;
queue_source->monitor = self;
queue_source->mainloop = mainloop;
g_source_set_name (source, "Message Queue Source");
return queue_source;
}
static gpointer
calendar_view_thread_func (gpointer data)
{
g_autoptr (GMainLoop) mainloop = NULL;
MessageQueueSource *queue_source;
GcalCalendarMonitor *self;
GCAL_ENTRY;
self = data;
mainloop = g_main_loop_new (self->thread_context, FALSE);
g_main_context_push_thread_default (self->thread_context);
/* Events source */
queue_source = views_monitor_source_new (self, mainloop);
g_source_attach ((GSource*) queue_source, self->thread_context);
g_main_loop_run (mainloop);
g_main_context_pop_thread_default (self->thread_context);
GCAL_RETURN (NULL);
}
/*
* Auxiliary methods
*/
static void
notify_view_thread (GcalCalendarMonitor *self,
MonitorThreadEvent event)
{
g_assert (GCAL_IS_MAIN_THREAD ());
g_async_queue_push (self->messages, GINT_TO_POINTER (event));
g_main_context_wakeup (self->thread_context);
}
static void
maybe_spawn_view_thread (GcalCalendarMonitor *self)
{
g_autofree gchar *thread_name = NULL;
g_assert (GCAL_IS_MAIN_THREAD ());
if (self->thread || !self->shared.range)
return;
thread_name = g_strdup_printf ("GcalCalendarMonitor (%s)", gcal_calendar_get_id (self->calendar));
self->thread = g_thread_new (thread_name, calendar_view_thread_func, self);
g_debug ("Spawning thread %s", thread_name);
}
static void
remove_events_outside_range (GcalCalendarMonitor *self,
GcalRange *range)
{
g_autoptr (GPtrArray) events_to_remove = NULL;
GHashTableIter iter;
GcalEvent *event;
g_assert (GCAL_IS_MAIN_THREAD ());
g_assert (range != NULL);
GCAL_TRACE_MSG ("Removing events outside range from monitor");
G_RW_LOCK_WRITER_AUTO_LOCK (&self->shared.lock, writer_locker);
events_to_remove = g_ptr_array_new_null_terminated (g_hash_table_size (self->shared.events),
g_object_unref,
TRUE);
g_hash_table_iter_init (&iter, self->shared.events);
while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &event))
{
GcalRange *event_range = gcal_event_get_range (event);
if (!self->shared.expand_recurrences && gcal_event_has_recurrence (event))
continue;
if (gcal_range_calculate_overlap (range, event_range, NULL) != GCAL_RANGE_NO_OVERLAP)
continue;
g_ptr_array_add (events_to_remove, event);
g_hash_table_iter_steal (&iter);
}
if (events_to_remove->len > 0)
gcal_event_list_remove_events (self->event_list, (GcalEvent **) events_to_remove->pdata);
}
static void
remove_all_events (GcalCalendarMonitor *self)
{
g_assert (GCAL_IS_MAIN_THREAD ());
GCAL_TRACE_MSG ("Removing all events from view");
G_RW_LOCK_WRITER_AUTO_LOCK (&self->shared.lock, writer_locker);
g_hash_table_remove_all (self->shared.events);
gcal_event_list_remove_all_events (self->event_list);
}
static void
set_complete (GcalCalendarMonitor *self,
gboolean complete)
{
g_assert (GCAL_IS_MAIN_THREAD ());
if (self->complete == complete)
return;
GCAL_TRACE_MSG ("Setting complete to %s", complete ? "TRUE" : "FALSE");
self->complete = complete;
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMPLETE]);
}
/*
* Callbacks
*/
static gboolean
add_events_to_timeline_in_idle_cb (gpointer user_data)
{
g_autoptr (GPtrArray) events_to_add = NULL;
GcalCalendarMonitor *self;
GPtrArray *events;
IdleData *idle_data;
GCAL_ENTRY;
g_assert (GCAL_IS_MAIN_THREAD ());
idle_data = user_data;
self = idle_data->monitor;
events = idle_data->events;
g_assert (idle_data->event_ids == NULL);
events_to_add = g_ptr_array_new_null_terminated (events->len, NULL, TRUE);
G_RW_LOCK_WRITER_AUTO_LOCK (&self->shared.lock, writer_locker);
for (guint i = 0; i < events->len; i++)
{
GcalEvent *event;
const gchar *uid;
event = g_ptr_array_index (events, i);
uid = gcal_event_get_uid (event);
if (!g_hash_table_contains (self->shared.events, uid))
{
g_hash_table_insert (self->shared.events, g_strdup (uid), g_object_ref (event));
g_ptr_array_add (events_to_add, event);
}
}
if (events_to_add->len > 0)
gcal_event_list_add_events (self->event_list, (GcalEvent **) events_to_add->pdata);
GCAL_RETURN (G_SOURCE_REMOVE);
}
static gboolean
update_events_in_idle_cb (gpointer user_data)
{
g_autoptr (GPtrArray) old_events = NULL;
g_autoptr (GPtrArray) new_events = NULL;
g_autoptr (GPtrArray) events = NULL;
GcalCalendarMonitor *self;
IdleData *idle_data;
GCAL_ENTRY;
g_assert (GCAL_IS_MAIN_THREAD ());
idle_data = user_data;
self = idle_data->monitor;
events = g_steal_pointer (&idle_data->events);
g_assert (idle_data->event_ids == NULL);
old_events = g_ptr_array_new_null_terminated (events->len, g_object_unref, TRUE);
new_events = g_ptr_array_new_null_terminated (events->len, NULL, TRUE);
G_RW_LOCK_WRITER_AUTO_LOCK (&self->shared.lock, writer_locker);
for (guint i = 0; i < events->len; i++)
{
g_autoptr (GcalEvent) old_event = NULL;
g_autofree gchar *old_event_id = NULL;
GcalEvent *event;
const gchar *event_id;
event = g_ptr_array_index (events, i);
event_id = gcal_event_get_uid (event);
if (g_hash_table_steal_extended (self->shared.events,
event_id,
(gpointer*) &old_event_id,
(gpointer*) &old_event))
{
g_hash_table_insert (self->shared.events, g_strdup (event_id), g_object_ref (event));
g_ptr_array_add (old_events, g_steal_pointer (&old_event));
g_ptr_array_add (new_events, event);
}
}
g_assert (old_events->len == new_events->len);
if (new_events->len > 0)
{
gcal_event_list_remove_events (self->event_list, (GcalEvent **) old_events->pdata);
gcal_event_list_add_events (self->event_list, (GcalEvent **) new_events->pdata);
}
GCAL_RETURN (G_SOURCE_REMOVE);
}
static gboolean
remove_events_from_timeline_in_idle_cb (gpointer user_data)
{
g_autoptr (GPtrArray) events_to_remove = NULL;
g_autoptr (GPtrArray) event_ids = NULL;
GcalCalendarMonitor *self;
IdleData *idle_data;
GCAL_ENTRY;
g_assert (GCAL_IS_MAIN_THREAD ());
idle_data = user_data;
self = idle_data->monitor;
event_ids = g_steal_pointer (&idle_data->event_ids);
g_assert (idle_data->events == NULL);
events_to_remove = g_ptr_array_new_null_terminated (event_ids->len, g_object_unref, TRUE);
G_RW_LOCK_WRITER_AUTO_LOCK (&self->shared.lock, writer_locker);
for (guint i = 0; i < event_ids->len; i++)
{
const gchar *event_id;
GcalEvent *event;
event_id = g_ptr_array_index (event_ids, i);
event = g_hash_table_lookup (self->shared.events, event_id);
if (event)
{
/* Keep the event alive until the listener process it*/
g_ptr_array_add (events_to_remove, g_object_ref (event));
g_hash_table_remove (self->shared.events, event_id);
}
}
if (events_to_remove->len > 0)
gcal_event_list_remove_events (self->event_list, (GcalEvent **) events_to_remove->pdata);
GCAL_RETURN (G_SOURCE_REMOVE);
}
static void
on_calendar_visible_changed_cb (GcalCalendar *calendar,
GParamSpec *pspec,
GcalCalendarMonitor *self)
{
if (gcal_calendar_get_visible (calendar))
{
notify_view_thread (self, CREATE_VIEW);
}
else
{
remove_all_events (self);
notify_view_thread (self, REMOVE_VIEW);
}
}
static gboolean
complete_in_idle_cb (gpointer user_data)
{
GcalCalendarMonitor *self;
IdleData *idle_data;
GCAL_ENTRY;
g_assert (GCAL_IS_MAIN_THREAD ());
idle_data = user_data;
self = idle_data->monitor;
g_assert (idle_data->events == NULL);
g_assert (idle_data->event_ids == NULL);
set_complete (self, idle_data->complete);
GCAL_RETURN (G_SOURCE_REMOVE);
}
/*
* GObject overrides
*/
static void
gcal_calendar_monitor_dispose (GObject *object)
{
GcalCalendarMonitor *self = (GcalCalendarMonitor *)object;
g_cancellable_cancel (self->cancellable);
notify_view_thread (self, QUIT);
if (self->thread)
{
g_thread_join (self->thread);
self->thread = NULL;
}
remove_all_events (self);
g_clear_object (&self->cancellable);
G_OBJECT_CLASS (gcal_calendar_monitor_parent_class)->dispose (object);
}
static void
gcal_calendar_monitor_finalize (GObject *object)
{
GcalCalendarMonitor *self = (GcalCalendarMonitor *)object;
g_clear_object (&self->event_list);
g_clear_object (&self->calendar);
g_clear_pointer (&self->thread_context, g_main_context_unref);
g_clear_pointer (&self->main_context, g_main_context_unref);
g_clear_pointer (&self->messages, g_async_queue_unref);
g_clear_pointer (&self->shared.events, g_hash_table_destroy);
g_clear_pointer (&self->shared.filter, g_free);
g_clear_pointer (&self->shared.range, gcal_range_unref);
G_OBJECT_CLASS (gcal_calendar_monitor_parent_class)->finalize (object);
}
static void
gcal_calendar_monitor_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GcalCalendarMonitor *self = GCAL_CALENDAR_MONITOR (object);
switch (prop_id)
{
case PROP_CALENDAR:
g_value_set_object (value, self->calendar);
break;
case PROP_COMPLETE:
g_value_set_boolean (value, self->complete);
break;
case PROP_EXPAND_RECURRENCES:
g_value_set_boolean (value, gcal_calendar_monitor_get_expand_recurrences (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
gcal_calendar_monitor_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GcalCalendarMonitor *self = GCAL_CALENDAR_MONITOR (object);
switch (prop_id)
{
case PROP_CALENDAR:
g_assert (self->calendar == NULL);
self->calendar = g_value_dup_object (value);
g_signal_connect (self->calendar,
"notify::visible",
G_CALLBACK (on_calendar_visible_changed_cb),
self);
break;
case PROP_EXPAND_RECURRENCES:
self->shared.expand_recurrences = g_value_get_boolean (value);
break;
case PROP_COMPLETE:
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
gcal_calendar_monitor_class_init (GcalCalendarMonitorClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->dispose = gcal_calendar_monitor_dispose;
object_class->finalize = gcal_calendar_monitor_finalize;
object_class->get_property = gcal_calendar_monitor_get_property;
object_class->set_property = gcal_calendar_monitor_set_property;
properties[PROP_CALENDAR] = g_param_spec_object ("calendar", NULL, NULL,
GCAL_TYPE_CALENDAR,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
properties[PROP_COMPLETE] = g_param_spec_boolean ("complete", NULL, NULL,
FALSE,
G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* GcalCalendarMonitor:expand-recurrences:
*
* Whether to expand recurrences into multiple instances.
*/
properties[PROP_EXPAND_RECURRENCES] = g_param_spec_boolean ("expand-recurrences",
NULL,
NULL,
TRUE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, N_PROPS, properties);
}
static void
gcal_calendar_monitor_init (GcalCalendarMonitor *self)
{
self->shared.expand_recurrences = TRUE;
self->shared.events = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
self->thread_context = g_main_context_new ();
self->main_context = g_main_context_ref_thread_default ();
self->messages = g_async_queue_new ();
self->complete = FALSE;
self->event_list = gcal_event_list_new ();
g_rw_lock_init (&self->shared.lock);
g_signal_connect_swapped (self->event_list, "items-changed", G_CALLBACK (g_list_model_items_changed), self);
}
GcalCalendarMonitor*
gcal_calendar_monitor_new (GcalCalendar *calendar)
{
g_assert (calendar != NULL && GCAL_IS_CALENDAR (calendar));
return g_object_new (GCAL_TYPE_CALENDAR_MONITOR,
"calendar", calendar,
NULL);
}
/**
* gcal_calendar_monitor_set_range:
* @self: a #GcalCalendarMonitor
* @range: a #GcalRange
*
* Updates the range of @self. This usually will result in
* @self creating a new view, and gathering the events from
* the events server.
*/
void
gcal_calendar_monitor_set_range (GcalCalendarMonitor *self,
GcalRange *range)
{
g_autoptr (GRWLockWriterLocker) writer_locker = NULL;
gboolean range_changed;
g_return_if_fail (GCAL_IS_CALENDAR_MONITOR (self));
GCAL_ENTRY;
writer_locker = g_rw_lock_writer_locker_new (&self->shared.lock);
range_changed =
!self->shared.range ||
!range ||
gcal_range_calculate_overlap (self->shared.range, range, NULL) != GCAL_RANGE_EQUAL;
if (!range_changed)
GCAL_RETURN ();
g_clear_pointer (&self->shared.range, gcal_range_unref);
self->shared.range = range ? gcal_range_copy (range) : NULL;
g_clear_pointer (&writer_locker, g_rw_lock_writer_locker_free);
maybe_spawn_view_thread (self);
if (range)
remove_events_outside_range (self, range);
else
remove_all_events (self);
g_cancellable_cancel (self->cancellable);
if (gcal_calendar_get_visible (self->calendar))
notify_view_thread (self, RANGE_UPDATED);
GCAL_EXIT;
}
/**
* gcal_calendar_monitor_get_cached_event:
* @self: a #GcalCalendarMonitor
* @event_id: the id of the #GcalEvent
*
* Returns: (transfer full)(nullable): a #GcalEvent, or %NULL
*/
GcalEvent*
gcal_calendar_monitor_get_cached_event (GcalCalendarMonitor *self,
const gchar *event_id)
{
GcalEvent *event;
g_return_val_if_fail (GCAL_IS_CALENDAR_MONITOR (self), NULL);
g_return_val_if_fail (event_id, NULL);
g_rw_lock_reader_lock (&self->shared.lock);
event = g_hash_table_lookup (self->shared.events, event_id);
g_rw_lock_reader_unlock (&self->shared.lock);
return event ? g_object_ref (event) : NULL;
}
void
gcal_calendar_monitor_set_filter (GcalCalendarMonitor *self,
const gchar *filter)
{
g_return_if_fail (GCAL_IS_CALENDAR_MONITOR (self));
g_rw_lock_writer_lock (&self->shared.lock);
g_clear_pointer (&self->shared.filter, g_free);
self->shared.filter = g_strdup (filter);
g_rw_lock_writer_unlock (&self->shared.lock);
remove_all_events (self);
if (gcal_calendar_get_visible (self->calendar))
notify_view_thread (self, FILTER_UPDATED);
}
gboolean
gcal_calendar_monitor_is_complete (GcalCalendarMonitor *self)
{
g_return_val_if_fail (GCAL_IS_CALENDAR_MONITOR (self), FALSE);
return self->complete;
}
gboolean
gcal_calendar_monitor_get_expand_recurrences (GcalCalendarMonitor *self)
{
g_return_val_if_fail (GCAL_IS_CALENDAR_MONITOR (self), TRUE);
return self->shared.expand_recurrences;
}