diff --git a/src/calls-plugin-manager.c b/src/calls-plugin-manager.c new file mode 100644 index 0000000..b327f48 --- /dev/null +++ b/src/calls-plugin-manager.c @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2023 Purism SPC + * + * This file is part of Calls. + * + * Calls 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. + * + * Calls 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 Calls. If not, see . + * + * Author: Evangelos Ribeiro Tzaras + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "CallsPluginManager" + +#include "calls-config.h" + +#include "calls-provider.h" +#include "calls-plugin.h" +#include "calls-plugin-manager.h" +#include "calls-util.h" + +#include + +/** + * SECTION:plugin-manager + * @short_description: Manages available plugins + * @Title: CallsPluginManager + * + * #CallsPluginManager is a singleton that manages available plugins. + */ + +struct _CallsPluginManager { + GObject parent_instance; + + PeasEngine *plugin_engine; + + GListStore *plugins; + GListStore *providers; +}; + +G_DEFINE_TYPE (CallsPluginManager, calls_plugin_manager, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_PROVIDERS, + PROP_PLUGINS, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +static CallsPlugin * +find_plugin_by_name (CallsPluginManager *self, + const char *module_name, + guint *position) +{ + guint n_plugins; + GListModel *plugins; + + g_assert (CALLS_IS_PLUGIN_MANAGER (self)); + + plugins = G_LIST_MODEL (self->plugins); + n_plugins = g_list_model_get_n_items (plugins); + + for (guint i = 0; i < n_plugins; i++) { + g_autoptr (CallsPlugin) plugin = g_list_model_get_item (plugins, i); + + if (g_strcmp0 (calls_plugin_get_module_name (plugin), module_name) == 0) { + if (position) + *position = i; + + return plugin; + } + } + + return NULL; +} + + +static void +on_plugin_loaded (CallsPlugin *plugin, + GParamSpec *pspec, + CallsPluginManager *self) +{ + CallsProvider *provider = calls_plugin_get_provider (plugin); + gboolean loaded = calls_plugin_is_loaded (plugin); + + if (!provider) { + g_debug ("Plugin '%s' was %sloaded, but no provider was found to %s", + calls_plugin_get_module_name (plugin), + loaded ? "" : "un", + loaded ? "add" : "remove"); + return; + } + + if (calls_plugin_is_loaded (plugin)) { + g_list_store_append (self->providers, provider); + } else { + guint pos; + + g_list_store_find (self->providers, provider, &pos); + g_list_store_remove (self->providers, pos); + } +} + + +static void +calls_plugin_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + CallsPluginManager *self = CALLS_PLUGIN_MANAGER (object); + + switch (property_id) { + case PROP_PROVIDERS: + g_value_set_object (value, calls_plugin_manager_get_providers (self)); + break; + + case PROP_PLUGINS: + g_value_set_object (value, calls_plugin_manager_get_plugins (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +unload_and_free_plugins (GListStore *plugins) +{ + GListModel *model = G_LIST_MODEL (plugins); + guint n = g_list_model_get_n_items (model); + for (guint i = 0; i < n; i++) { + g_autoptr (CallsPlugin) plugin = g_list_model_get_item (model, i); + g_autoptr (GError) error = NULL; + + if (!calls_plugin_unload (plugin, &error)) + g_warning ("Could not unload plugin '%s': %s", + calls_plugin_get_module_name (plugin), + error->message); + } + + g_object_unref (plugins); +} + +static void +calls_plugin_manager_dispose (GObject *object) +{ + CallsPluginManager *self = CALLS_PLUGIN_MANAGER (object); + + g_clear_pointer (&self->plugins, unload_and_free_plugins); + g_clear_object (&self->providers); + g_clear_object (&self->plugin_engine); + + G_OBJECT_CLASS (calls_plugin_manager_parent_class)->dispose (object); +} + + +static void +calls_plugin_manager_class_init (CallsPluginManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = calls_plugin_manager_dispose; + object_class->get_property = calls_plugin_manager_get_property; + + /** + * CallsPluginManager:providers: + * + * A #GListModel of #CallsProvider that are currently loaded. + */ + props[PROP_PROVIDERS] = + g_param_spec_object ("providers", + "", + "", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * CallsPluginManager:plugins: + * + * A #GListModel of #CallsProvider that are currently loaded. + */ + props[PROP_PLUGINS] = + g_param_spec_object ("plugins", + "", + "", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +calls_plugin_manager_init (CallsPluginManager *self) +{ + g_autofree char *default_plugin_dir_provider = NULL; + const char *dir; + + self->plugin_engine = peas_engine_new (); + + dir = g_getenv ("CALLS_PLUGIN_DIR"); + if (!STR_IS_NULL_OR_EMPTY (dir)) { + g_autofree char *plugin_dir_provider = NULL; + + plugin_dir_provider = g_build_filename (dir, "provider", NULL); + + if (g_file_test (plugin_dir_provider, G_FILE_TEST_EXISTS)) { + g_debug ("Adding '%s' to plugin search path", plugin_dir_provider); + peas_engine_prepend_search_path (self->plugin_engine, plugin_dir_provider, NULL); + } else { + g_warning ("Not adding '%s' to plugin search path, because the directory doesn't exist.\n" + "Check if env CALLS_PLUGIN_DIR is set correctly", plugin_dir_provider); + } + } + + default_plugin_dir_provider = g_build_filename (PLUGIN_LIBDIR, "provider", NULL); + peas_engine_add_search_path (self->plugin_engine, default_plugin_dir_provider, NULL); + g_debug ("Scanning for plugins in '%s'", default_plugin_dir_provider); + + peas_engine_rescan_plugins (self->plugin_engine); + + self->plugins = g_list_store_new (CALLS_TYPE_PLUGIN); + + self->providers = g_list_store_new (CALLS_TYPE_PROVIDER); + + for (const GList *node = peas_engine_get_plugin_list (self->plugin_engine); node; node = node->next) { + CallsPlugin *plugin = calls_plugin_new (node->data); + + g_signal_connect_object (plugin, + "notify::loaded", + G_CALLBACK (on_plugin_loaded), + self, + G_CONNECT_AFTER); + + g_list_store_append (self->plugins, plugin); + } +} + + +CallsPluginManager * +calls_plugin_manager_get_default (void) +{ + static CallsPluginManager *instance; + + if (instance == NULL) { + instance = g_object_new (CALLS_TYPE_PLUGIN_MANAGER, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *) &instance); + } + return instance; +} + + +gboolean +calls_plugin_manager_load_plugin (CallsPluginManager *self, + const char *name, + GError **error) +{ + CallsPlugin *plugin; + + g_return_val_if_fail (CALLS_IS_PLUGIN_MANAGER (self), FALSE); + g_return_val_if_fail (!STR_IS_NULL_OR_EMPTY (name), FALSE); + + plugin = find_plugin_by_name (self, name, NULL); + + if (!plugin) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "No plugin '%s' found", name); + return FALSE; + } + + return calls_plugin_load (plugin, error); +} + + +gboolean +calls_plugin_manager_unload_plugin (CallsPluginManager *self, + const char *name, + GError **error) +{ + CallsPlugin *plugin; + + g_return_val_if_fail (CALLS_IS_PLUGIN_MANAGER (self), FALSE); + g_return_val_if_fail (!STR_IS_NULL_OR_EMPTY (name), FALSE); + + plugin = find_plugin_by_name (self, name, NULL); + + if (!plugin) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "No plugin '%s' found", name); + return FALSE; + } + + return calls_plugin_unload (plugin, error); +} + +/** + * calls_plugin_manager_get_plugins: + * @self: The #CallsPluginManager + * + * Returns: (transfer none): A #GListModel of #[CallsPlugin]s + */ +GListModel * +calls_plugin_manager_get_plugins (CallsPluginManager *self) +{ + g_return_val_if_fail (CALLS_IS_PLUGIN_MANAGER (self), NULL); + + return G_LIST_MODEL (self->plugins); +} +/** + * calls_plugin_manager_get_providers: + * @self: The #CallsPluginManager + * + * Returns: (transfer none): A #GListModel of #[CallsProvider]s + */ +GListModel * +calls_plugin_manager_get_providers (CallsPluginManager *self) +{ + g_return_val_if_fail (CALLS_IS_PLUGIN_MANAGER (self), NULL); + + return G_LIST_MODEL (self->providers); +} + + +/** + * calls_plugin_manager_has_any_plugins: + * @self: The #CallsPluginManager + * + * Returns: %TRUE if any plugin is loaded, %FALSE otherwise. + */ +gboolean +calls_plugin_manager_has_any_plugins (CallsPluginManager *self) +{ + guint n_plugins; + + g_return_val_if_fail (CALLS_IS_PLUGIN_MANAGER (self), FALSE); + + n_plugins = g_list_model_get_n_items (G_LIST_MODEL (self->plugins)); + + for (guint i = 0; i < n_plugins; i++) { + g_autoptr (CallsPlugin) plugin = + g_list_model_get_item (G_LIST_MODEL (self->plugins), i); + + if (calls_plugin_is_loaded (plugin)) + return TRUE; + } + return FALSE; +} + +/** + * calls_plugin_manager_has_plugins: + * @self: The #CallsPluginManager + * @name: The name of the plugin to check + * + * Returns: %TRUE if a plugin with @name is loaded, %FALSE otherwise. + */ +gboolean +calls_plugin_manager_has_plugin (CallsPluginManager *self, + const char *name) +{ + CallsPlugin *plugin; + + g_return_val_if_fail (CALLS_IS_PLUGIN_MANAGER (self), FALSE); + g_return_val_if_fail (!STR_IS_NULL_OR_EMPTY (name), FALSE); + + plugin = find_plugin_by_name (self, name, NULL); + if (!plugin) + return FALSE; + + return calls_plugin_is_loaded (plugin); +} + + +/** + * calls_plugin_manager_get_plugin_names: + * @self: The #CallsPluginManager + * @length: (optional) (out): the length of the returned array + * + * Retrieves the names of all plugins loaded by @self, as an array. + * + * You should free the return value with g_free(). + * + * Returns: (array length=length) (transfer container): a + * %NULL-terminated array containing the plugin names. + */ +const char ** +calls_plugin_manager_get_plugin_names (CallsPluginManager *self, + guint *length) +{ + GPtrArray *array; + guint n_items; + + g_return_val_if_fail (CALLS_IS_PLUGIN_MANAGER (self), NULL); + + n_items = g_list_model_get_n_items (G_LIST_MODEL (self->plugins)); + if (length) + *length = n_items; + array = g_ptr_array_sized_new (n_items + 1); + array->pdata[n_items] = NULL; + + for (guint i = 0; i < n_items; i++) { + g_autoptr (CallsPlugin) plugin = + g_list_model_get_item (G_LIST_MODEL (self->plugins), i); + + array->pdata[i] = (gpointer) calls_plugin_get_module_name (plugin); + } + + return (const char **) g_ptr_array_free (array, FALSE); +} diff --git a/src/calls-plugin-manager.h b/src/calls-plugin-manager.h new file mode 100644 index 0000000..1bab668 --- /dev/null +++ b/src/calls-plugin-manager.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Purism SPC + * + * This file is part of Calls. + * + * Calls 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. + * + * Calls 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 Calls. If not, see . + * + * Authors: Evangelos Ribeiro Tzaras + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define CALLS_TYPE_PLUGIN_MANAGER (calls_plugin_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (CallsPluginManager, calls_plugin_manager, CALLS, PLUGIN_MANAGER, GObject) + + +CallsPluginManager *calls_plugin_manager_get_default (void); +gboolean calls_plugin_manager_load_plugin (CallsPluginManager *self, + const char *name, + GError **error); +gboolean calls_plugin_manager_unload_plugin (CallsPluginManager *self, + const char *name, + GError **error); +const char **calls_plugin_manager_get_plugin_names (CallsPluginManager *self, + guint *length); +gboolean calls_plugin_manager_has_plugin (CallsPluginManager *self, + const char *name); +gboolean calls_plugin_manager_has_any_plugins (CallsPluginManager *self); +GListModel *calls_plugin_manager_get_plugins (CallsPluginManager *self); +GListModel *calls_plugin_manager_get_providers (CallsPluginManager *self); + +G_END_DECLS diff --git a/src/meson.build b/src/meson.build index e1d747f..06e495d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -117,6 +117,7 @@ calls_sources = files([ 'calls-notifier.c', 'calls-notifier.h', 'calls-origin.c', 'calls-origin.h', 'calls-plugin.c', 'calls-plugin.h', + 'calls-plugin-manager.c', 'calls-plugin-manager.h', 'calls-provider.c', 'calls-provider.h', 'calls-record-store.c', 'calls-record-store.h', 'calls-ringer.c', 'calls-ringer.h',