/* * e-source-refresh.c * * This library is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . * */ /** * SECTION: e-source-refresh * @include: libedataserver/libedataserver.h * @short_description: #ESource extension for refresh settings * * The #ESourceRefresh extension tracks the interval for fetching * updates from a remote server. * * Access the extension as follows: * * |[ * #include * * ESourceRefresh *extension; * * extension = e_source_get_extension (source, E_SOURCE_EXTENSION_REFRESH); * ]| **/ #include "e-source-refresh.h" #define E_SOURCE_REFRESH_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), E_TYPE_SOURCE_REFRESH, ESourceRefreshPrivate)) typedef struct _TimeoutNode TimeoutNode; struct _ESourceRefreshPrivate { gboolean enabled; guint interval_minutes; GMutex timeout_lock; GHashTable *timeout_table; guint next_timeout_id; }; struct _TimeoutNode { GSource *source; GMainContext *context; ESourceRefresh *extension; /* not referenced */ ESourceRefreshFunc callback; gpointer user_data; GDestroyNotify notify; }; enum { PROP_0, PROP_ENABLED, PROP_INTERVAL_MINUTES }; G_DEFINE_TYPE ( ESourceRefresh, e_source_refresh, E_TYPE_SOURCE_EXTENSION) static TimeoutNode * timeout_node_new (ESourceRefresh *extension, GMainContext *context, ESourceRefreshFunc callback, gpointer user_data, GDestroyNotify notify) { TimeoutNode *node; if (context != NULL) g_main_context_ref (context); node = g_slice_new0 (TimeoutNode); node->context = context; node->callback = callback; node->user_data = user_data; node->notify = notify; /* Do not reference. The timeout node will * not outlive the ESourceRefresh extension. */ node->extension = extension; return node; } static gboolean timeout_node_invoke (gpointer data) { TimeoutNode *node = data; ESourceExtension *extension; ESource *source; extension = E_SOURCE_EXTENSION (node->extension); source = e_source_extension_ref_source (extension); g_return_val_if_fail (source != NULL, FALSE); /* We allow timeouts to be scheduled for disabled data sources * but we don't invoke the callback. Keeps the logic simple. */ if (e_source_get_enabled (source)) node->callback (source, node->user_data); g_object_unref (source); return TRUE; } static void timeout_node_attach (TimeoutNode *node) { guint interval_minutes; if (node->source != NULL) return; interval_minutes = e_source_refresh_get_interval_minutes (node->extension); node->source = g_timeout_source_new_seconds (interval_minutes * 60); g_source_set_callback ( node->source, timeout_node_invoke, node, (GDestroyNotify) NULL); g_source_attach (node->source, node->context); } static void timeout_node_detach (TimeoutNode *node) { if (node->source == NULL) return; g_source_destroy (node->source); g_source_unref (node->source); node->source = NULL; } static void timeout_node_free (TimeoutNode *node) { if (node->source != NULL) timeout_node_detach (node); if (node->context != NULL) g_main_context_unref (node->context); if (node->notify != NULL) node->notify (node->user_data); g_slice_free (TimeoutNode, node); } static void source_refresh_update_timeouts (ESourceRefresh *extension, gboolean invoke_callbacks) { GList *list, *link; g_mutex_lock (&extension->priv->timeout_lock); list = g_hash_table_get_values (extension->priv->timeout_table); for (link = list; link != NULL; link = g_list_next (link)) { TimeoutNode *node = link->data; timeout_node_detach (node); if (invoke_callbacks) timeout_node_invoke (node); if (e_source_refresh_get_enabled (extension)) timeout_node_attach (node); } g_list_free (list); g_mutex_unlock (&extension->priv->timeout_lock); } static gboolean source_refresh_idle_cb (gpointer user_data) { ESource *source = E_SOURCE (user_data); if (e_source_get_enabled (source)) e_source_refresh_force_timeout (source); return FALSE; } static void source_refresh_notify_enabled_cb (ESource *source, GParamSpec *pspec, ESourceRefresh *extension) { GSource *idle_source; GMainContext *main_context; main_context = e_source_ref_main_context (source); idle_source = g_idle_source_new (); g_source_set_callback ( idle_source, source_refresh_idle_cb, g_object_ref (source), (GDestroyNotify) g_object_unref); g_source_attach (idle_source, main_context); g_source_unref (idle_source); g_main_context_unref (main_context); } static void source_refresh_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_ENABLED: e_source_refresh_set_enabled ( E_SOURCE_REFRESH (object), g_value_get_boolean (value)); return; case PROP_INTERVAL_MINUTES: e_source_refresh_set_interval_minutes ( E_SOURCE_REFRESH (object), g_value_get_uint (value)); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void source_refresh_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_ENABLED: g_value_set_boolean ( value, e_source_refresh_get_enabled ( E_SOURCE_REFRESH (object))); return; case PROP_INTERVAL_MINUTES: g_value_set_uint ( value, e_source_refresh_get_interval_minutes ( E_SOURCE_REFRESH (object))); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void source_refresh_dispose (GObject *object) { ESourceRefreshPrivate *priv; priv = E_SOURCE_REFRESH_GET_PRIVATE (object); g_hash_table_remove_all (priv->timeout_table); /* Chain up to parent's dispose() method. */ G_OBJECT_CLASS (e_source_refresh_parent_class)->dispose (object); } static void source_refresh_finalize (GObject *object) { ESourceRefreshPrivate *priv; priv = E_SOURCE_REFRESH_GET_PRIVATE (object); g_mutex_clear (&priv->timeout_lock); g_hash_table_destroy (priv->timeout_table); /* Chain up to parent's finalize() method. */ G_OBJECT_CLASS (e_source_refresh_parent_class)->finalize (object); } static void source_refresh_constructed (GObject *object) { ESourceExtension *extension; ESource *source; /* Chain up to parent's constructed() method. */ G_OBJECT_CLASS (e_source_refresh_parent_class)->constructed (object); extension = E_SOURCE_EXTENSION (object); source = e_source_extension_ref_source (extension); /* There should be no lifecycle issues here * since we get finalized with our ESource. */ g_signal_connect ( source, "notify::enabled", G_CALLBACK (source_refresh_notify_enabled_cb), extension); g_object_unref (source); } static void e_source_refresh_class_init (ESourceRefreshClass *class) { GObjectClass *object_class; ESourceExtensionClass *extension_class; g_type_class_add_private (class, sizeof (ESourceRefreshPrivate)); object_class = G_OBJECT_CLASS (class); object_class->set_property = source_refresh_set_property; object_class->get_property = source_refresh_get_property; object_class->dispose = source_refresh_dispose; object_class->finalize = source_refresh_finalize; object_class->constructed = source_refresh_constructed; extension_class = E_SOURCE_EXTENSION_CLASS (class); extension_class->name = E_SOURCE_EXTENSION_REFRESH; g_object_class_install_property ( object_class, PROP_ENABLED, g_param_spec_boolean ( "enabled", "Enabled", "Whether to periodically refresh", TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS | E_SOURCE_PARAM_SETTING)); g_object_class_install_property ( object_class, PROP_INTERVAL_MINUTES, g_param_spec_uint ( "interval-minutes", "Interval in Minutes", "Refresh interval in minutes", 0, G_MAXUINT, 60, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS | E_SOURCE_PARAM_SETTING)); } static void e_source_refresh_init (ESourceRefresh *extension) { GHashTable *timeout_table; timeout_table = g_hash_table_new_full ( (GHashFunc) g_direct_hash, (GEqualFunc) g_direct_equal, (GDestroyNotify) NULL, (GDestroyNotify) timeout_node_free); extension->priv = E_SOURCE_REFRESH_GET_PRIVATE (extension); g_mutex_init (&extension->priv->timeout_lock); extension->priv->timeout_table = timeout_table; extension->priv->next_timeout_id = 1; } /** * e_source_refresh_get_enabled: * @extension: an #ESourceRefresh * * Returns whether to periodically fetch updates from a remote server. * * The refresh interval is determined by the #ESourceRefresh:interval-minutes * property. * * Returns: whether periodic refresh is enabled * * Since: 3.6 **/ gboolean e_source_refresh_get_enabled (ESourceRefresh *extension) { g_return_val_if_fail (E_IS_SOURCE_REFRESH (extension), FALSE); return extension->priv->enabled; } /** * e_source_refresh_set_enabled: * @extension: an #ESourceRefresh * @enabled: whether to enable periodic refresh * * Sets whether to periodically fetch updates from a remote server. * * The refresh interval is determined by the #ESourceRefresh:interval-minutes * property. * * Since: 3.6 **/ void e_source_refresh_set_enabled (ESourceRefresh *extension, gboolean enabled) { g_return_if_fail (E_IS_SOURCE_REFRESH (extension)); extension->priv->enabled = enabled; g_object_notify (G_OBJECT (extension), "enabled"); source_refresh_update_timeouts (extension, FALSE); } /** * e_source_refresh_get_interval_minutes: * @extension: an #ESourceRefresh * * Returns the interval for fetching updates from a remote server. * * Note this value is only effective when the #ESourceRefresh:enabled * property is %TRUE. * * Returns: the interval in minutes * * Since: 3.6 **/ guint e_source_refresh_get_interval_minutes (ESourceRefresh *extension) { g_return_val_if_fail (E_IS_SOURCE_REFRESH (extension), FALSE); return extension->priv->interval_minutes; } /** * e_source_refresh_set_interval_minutes: * @extension: an #ESourceRefresh * @interval_minutes: the interval in minutes * * Sets the interval for fetching updates from a remote server. * * Note this value is only effective when the #ESourceRefresh:enabled * property is %TRUE. * * Since: 3.6 **/ void e_source_refresh_set_interval_minutes (ESourceRefresh *extension, guint interval_minutes) { g_return_if_fail (E_IS_SOURCE_REFRESH (extension)); if (interval_minutes == extension->priv->interval_minutes) return; extension->priv->interval_minutes = interval_minutes; g_object_notify (G_OBJECT (extension), "interval-minutes"); source_refresh_update_timeouts (extension, FALSE); } /** * e_source_refresh_add_timeout: * @source: an #ESource * @context: (allow-none): a #GMainContext, or %NULL (if %NULL, the default * context will be used) * @callback: function to call on each timeout * @user_data: data to pass to @callback * @notify: (allow-none): function to call when the timeout is removed, * or %NULL * * This is a simple way to schedule a periodic data source refresh. * * Adds a timeout #GSource to @context and handles all the bookkeeping * if @source's refresh #ESourceRefresh:enabled state or its refresh * #ESourceRefresh:interval-minutes value changes. The @callback is * expected to dispatch an asynchronous job to connect to and fetch * updates from a remote server. * * The returned ID can be passed to e_source_refresh_remove_timeout() to * remove the timeout from @context. Note the ID is a private handle and * cannot be passed to g_source_remove(). * * Returns: a refresh timeout ID * * Since: 3.6 **/ guint e_source_refresh_add_timeout (ESource *source, GMainContext *context, ESourceRefreshFunc callback, gpointer user_data, GDestroyNotify notify) { ESourceRefresh *extension; const gchar *extension_name; TimeoutNode *node; guint timeout_id; gpointer key; g_return_val_if_fail (E_IS_SOURCE (source), 0); g_return_val_if_fail (callback != NULL, 0); extension_name = E_SOURCE_EXTENSION_REFRESH; extension = e_source_get_extension (source, extension_name); g_mutex_lock (&extension->priv->timeout_lock); timeout_id = extension->priv->next_timeout_id++; key = GUINT_TO_POINTER (timeout_id); node = timeout_node_new ( extension, context, callback, user_data, notify); g_hash_table_insert (extension->priv->timeout_table, key, node); if (e_source_refresh_get_enabled (extension)) timeout_node_attach (node); g_mutex_unlock (&extension->priv->timeout_lock); return timeout_id; } /** * e_source_refresh_force_timeout: * @source: an #ESource * * For all timeouts added with e_source_refresh_add_timeout(), invokes * the #ESourceRefreshFunc callback immediately and then, if the refresh * #ESourceRefresh:enabled state is TRUE, reschedules the timeout. * * This function is called automatically when the #ESource switches from * disabled to enabled, but can also be useful when a network connection * becomes available or when waking up from hibernation or suspend. * * Since: 3.6 **/ void e_source_refresh_force_timeout (ESource *source) { ESourceRefresh *extension; const gchar *extension_name; g_return_if_fail (E_IS_SOURCE (source)); extension_name = E_SOURCE_EXTENSION_REFRESH; extension = e_source_get_extension (source, extension_name); source_refresh_update_timeouts (extension, TRUE); } /** * e_source_refresh_remove_timeout: * @source: an #ESource * @refresh_timeout_id: a refresh timeout ID * * Removes a timeout #GSource added by e_source_refresh_add_timeout(). * * Returns: %TRUE if the timeout was found and removed * * Since: 3.6 **/ gboolean e_source_refresh_remove_timeout (ESource *source, guint refresh_timeout_id) { ESourceRefresh *extension; const gchar *extension_name; gboolean removed; gpointer key; g_return_val_if_fail (E_IS_SOURCE (source), FALSE); g_return_val_if_fail (refresh_timeout_id > 0, FALSE); extension_name = E_SOURCE_EXTENSION_REFRESH; extension = e_source_get_extension (source, extension_name); g_mutex_lock (&extension->priv->timeout_lock); key = GUINT_TO_POINTER (refresh_timeout_id); removed = g_hash_table_remove (extension->priv->timeout_table, key); g_mutex_unlock (&extension->priv->timeout_lock); return removed; } /** * e_source_refresh_remove_timeouts_by_data: * @source: an #ESource * @user_data: user data to match against timeout callbacks * * Removes all timeout #GSource's added by e_source_refresh_add_timeout() * whose callback data pointer matches @user_data. * * Returns: the number of timeouts found and removed * * Since: 3.6 **/ guint e_source_refresh_remove_timeouts_by_data (ESource *source, gpointer user_data) { ESourceRefresh *extension; const gchar *extension_name; GQueue trash = G_QUEUE_INIT; GHashTableIter iter; gpointer key, value; guint n_removed = 0; g_return_val_if_fail (E_IS_SOURCE (source), 0); extension_name = E_SOURCE_EXTENSION_REFRESH; extension = e_source_get_extension (source, extension_name); g_mutex_lock (&extension->priv->timeout_lock); g_hash_table_iter_init (&iter, extension->priv->timeout_table); while (g_hash_table_iter_next (&iter, &key, &value)) { TimeoutNode *node = value; if (node->user_data == user_data) g_queue_push_tail (&trash, key); } while ((key = g_queue_pop_head (&trash)) != NULL) if (g_hash_table_remove (extension->priv->timeout_table, key)) n_removed++; g_mutex_unlock (&extension->priv->timeout_lock); return n_removed; }