From 6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:57:27 +0200 Subject: Adding upstream version 43.5. Signed-off-by: Daniel Baumann --- src/gs-application.c | 1525 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1525 insertions(+) create mode 100644 src/gs-application.c (limited to 'src/gs-application.c') diff --git a/src/gs-application.c b/src/gs-application.c new file mode 100644 index 0000000..961c400 --- /dev/null +++ b/src/gs-application.c @@ -0,0 +1,1525 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Matthias Clasen + * Copyright (C) 2013-2018 Richard Hughes + * Copyright (C) 2014-2018 Kalev Lember + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-application.h" + +#include +#include +#include +#include + +#ifdef HAVE_PACKAGEKIT +#include "gs-dbus-helper.h" +#endif + +#include "gs-build-ident.h" +#include "gs-common.h" +#include "gs-debug.h" +#include "gs-shell.h" +#include "gs-update-monitor.h" +#include "gs-shell-search-provider.h" + +#define ENABLE_REPOS_DIALOG_CONF_KEY "enable-repos-dialog" + +struct _GsApplication { + AdwApplication parent; + GCancellable *cancellable; + GsPluginLoader *plugin_loader; + gint pending_apps; + GtkWindow *main_window; + GsShell *shell; + GsUpdateMonitor *update_monitor; +#ifdef HAVE_PACKAGEKIT + GsDbusHelper *dbus_helper; +#endif + GsShellSearchProvider *search_provider; /* (nullable) (owned) */ + GSettings *settings; + GSimpleActionGroup *action_map; + guint shell_loaded_handler_id; + GsDebug *debug; /* (owned) (not nullable) */ + + /* Created/freed on demand */ + GHashTable *withdraw_notifications; /* gchar *notification_id ~> GUINT_TO_POINTER (timeout_id) */ +}; + +G_DEFINE_TYPE (GsApplication, gs_application, ADW_TYPE_APPLICATION); + +typedef enum { + PROP_DEBUG = 1, +} GsApplicationProperty; + +static GParamSpec *obj_props[PROP_DEBUG + 1] = { NULL, }; + +enum { + INSTALL_RESOURCES_DONE, + REPOSITORY_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +static const char * +get_version (void) +{ + if (g_strcmp0 (BUILD_TYPE, "release") == 0) + return VERSION; + else + return GS_BUILD_IDENTIFIER; +} + +typedef struct { + GsApplication *app; + GSimpleAction *action; + GVariant *action_param; /* (nullable) */ +} GsActivationHelper; + +static GsActivationHelper * +gs_activation_helper_new (GsApplication *app, + GSimpleAction *action, + GVariant *parameter) +{ + GsActivationHelper *helper = g_slice_new0 (GsActivationHelper); + helper->app = app; + helper->action = G_SIMPLE_ACTION (action); + helper->action_param = (parameter != NULL) ? g_variant_ref_sink (parameter) : NULL; + + return helper; +} + +static void +gs_activation_helper_free (GsActivationHelper *helper) +{ + g_clear_pointer (&helper->action_param, g_variant_unref); + g_slice_free (GsActivationHelper, helper); +} + +gboolean +gs_application_has_active_window (GsApplication *application) +{ + GList *windows; + + windows = gtk_application_get_windows (GTK_APPLICATION (application)); + for (GList *l = windows; l != NULL; l = l->next) { + if (gtk_window_is_active (GTK_WINDOW (l->data))) + return TRUE; + } + return FALSE; +} + +static void +gs_application_init (GsApplication *application) +{ + const GOptionEntry options[] = { + { "mode", '\0', 0, G_OPTION_ARG_STRING, NULL, + /* TRANSLATORS: this is a command line option */ + _("Start up mode: either ‘updates’, ‘updated’, ‘installed’ or ‘overview’"), _("MODE") }, + { "search", '\0', 0, G_OPTION_ARG_STRING, NULL, + _("Search for applications"), _("SEARCH") }, + { "details", '\0', 0, G_OPTION_ARG_STRING, NULL, + _("Show application details (using application ID)"), _("ID") }, + { "details-pkg", '\0', 0, G_OPTION_ARG_STRING, NULL, + _("Show application details (using package name)"), _("PKGNAME") }, + { "install", '\0', 0, G_OPTION_ARG_STRING, NULL, + _("Install the application (using application ID)"), _("ID") }, + { "uninstall", '\0', 0, G_OPTION_ARG_STRING, NULL, + _("Uninstall the application (using application ID)"), _("ID") }, + { "local-filename", '\0', 0, G_OPTION_ARG_FILENAME, NULL, + _("Open a local package file"), _("FILENAME") }, + { "interaction", '\0', 0, G_OPTION_ARG_STRING, NULL, + _("The kind of interaction expected for this action: either " + "‘none’, ‘notify’, or ‘full’"), NULL }, + { "show-metainfo", '\0', 0, G_OPTION_ARG_FILENAME, NULL, + _("Show a local metainfo or appdata file"), _("FILENAME") }, + { "verbose", '\0', 0, G_OPTION_ARG_NONE, NULL, + _("Show verbose debugging information"), NULL }, + { "autoupdate", 0, 0, G_OPTION_ARG_NONE, NULL, + _("Installs any pending updates in the background"), NULL }, + { "prefs", 0, 0, G_OPTION_ARG_NONE, NULL, + _("Show update preferences"), NULL }, + { "quit", 0, 0, G_OPTION_ARG_NONE, NULL, + _("Quit the running instance"), NULL }, + { "prefer-local", '\0', 0, G_OPTION_ARG_NONE, NULL, + _("Prefer local file sources to AppStream"), NULL }, + { "version", 0, 0, G_OPTION_ARG_NONE, NULL, + _("Show version number"), NULL }, + { NULL } + }; + + g_application_add_main_option_entries (G_APPLICATION (application), options); +} + +static gboolean +gs_application_dbus_register (GApplication *application, + GDBusConnection *connection, + const gchar *object_path, + GError **error) +{ + GsApplication *app = GS_APPLICATION (application); + app->search_provider = gs_shell_search_provider_new (); + return gs_shell_search_provider_register (app->search_provider, connection, error); +} + +static void +gs_application_dbus_unregister (GApplication *application, + GDBusConnection *connection, + const gchar *object_path) +{ + GsApplication *app = GS_APPLICATION (application); + + if (app->search_provider != NULL) + gs_shell_search_provider_unregister (app->search_provider); +} + +static void +gs_application_shutdown (GApplication *application) +{ + GsApplication *app = GS_APPLICATION (application); + + g_cancellable_cancel (app->cancellable); + g_clear_object (&app->cancellable); + + g_clear_object (&app->shell); + + G_APPLICATION_CLASS (gs_application_parent_class)->shutdown (application); +} + +static void +gs_application_shell_loaded_cb (GsShell *shell, GsApplication *app) +{ + g_signal_handler_disconnect (app->shell, app->shell_loaded_handler_id); + app->shell_loaded_handler_id = 0; +} + +static void +gs_application_present_window (GsApplication *app, const gchar *startup_id) +{ + GList *windows; + GtkWindow *window; + + windows = gtk_application_get_windows (GTK_APPLICATION (app)); + if (windows) { + window = windows->data; + + if (startup_id != NULL) + gtk_window_set_startup_id (window, startup_id); + gtk_window_present (window); + } +} + +static void +sources_activated (GSimpleAction *action, + GVariant *parameter, + gpointer app) +{ + gs_shell_show_sources (GS_APPLICATION (app)->shell); +} + +static void +prefs_activated (GSimpleAction *action, GVariant *parameter, gpointer app) +{ + gs_shell_show_prefs (GS_APPLICATION (app)->shell); +} + +static void +about_activated (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + GsApplication *app = GS_APPLICATION (user_data); + const gchar *developers[] = { + "Richard Hughes", + "Matthias Clasen", + "Kalev Lember", + "Allan Day", + "Ryan Lerch", + "William Jon McCann", + "Milan Crha", + "Joaquim Rocha", + "Robert Ancell", + "Philip Withnall", + NULL + }; + +#if ADW_CHECK_VERSION(1,2,0) + adw_show_about_window (app->main_window, + "application-name", g_get_application_name (), + "application-icon", APPLICATION_ID, + "developer-name", _("The GNOME Project"), + "version", get_version(), + "website", "https://wiki.gnome.org/Apps/Software", + "issue-url", "https://gitlab.gnome.org/GNOME/gnome-software/-/issues/new", + "developers", developers, + "copyright", _("Copyright \xc2\xa9 2016–2022 GNOME Software contributors"), + "license-type", GTK_LICENSE_GPL_2_0, + "translator-credits", _("translator-credits"), + NULL); +#else + GtkAboutDialog *dialog; + dialog = GTK_ABOUT_DIALOG (gtk_about_dialog_new ()); + gtk_about_dialog_set_authors (dialog, developers); + gtk_about_dialog_set_copyright (dialog, _("Copyright \xc2\xa9 2016–2022 GNOME Software contributors")); + gtk_about_dialog_set_license_type (dialog, GTK_LICENSE_GPL_2_0); + gtk_about_dialog_set_logo_icon_name (dialog, APPLICATION_ID); + gtk_about_dialog_set_translator_credits (dialog, _("translator-credits")); + gtk_about_dialog_set_version (dialog, get_version ()); + gtk_about_dialog_set_program_name (dialog, g_get_application_name ()); + + /* TRANSLATORS: this is the title of the about window */ + gtk_window_set_title (GTK_WINDOW (dialog), _("About Software")); + + /* TRANSLATORS: well, we seem to think so, anyway */ + gtk_about_dialog_set_comments (dialog, _("A nice way to manage the " + "software on your system.")); + + gs_shell_modal_dialog_present (app->shell, GTK_WINDOW (dialog)); +#endif +} + +static void +cancel_trigger_failed_cb (GObject *source, GAsyncResult *res, gpointer user_data) +{ + GsApplication *app = GS_APPLICATION (user_data); + g_autoptr(GError) error = NULL; + if (!gs_plugin_loader_job_action_finish (app->plugin_loader, res, &error)) { + g_warning ("failed to cancel trigger: %s", error->message); + return; + } +} + +static void +reboot_failed_cb (GObject *source, GAsyncResult *res, gpointer user_data) +{ + GsApplication *app = GS_APPLICATION (user_data); + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get result */ + if (gs_utils_invoke_reboot_finish (source, res, &error)) + return; + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Calling reboot had been cancelled"); + else if (error != NULL) + g_warning ("Calling reboot failed: %s", error->message); + + /* cancel trigger */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE_CANCEL, NULL); + gs_plugin_loader_job_process_async (app->plugin_loader, plugin_job, + app->cancellable, + cancel_trigger_failed_cb, + app); +} + +static void +reboot_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + gs_utils_invoke_reboot_async (NULL, NULL, NULL); +} + +static void +shutdown_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + g_application_quit (G_APPLICATION (app)); +} + +static void offline_update_cb (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsApplication *app); + +static void +reboot_and_install (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + g_autoptr(GsPluginJob) plugin_job = NULL; + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, NULL); + gs_plugin_loader_job_process_async (app->plugin_loader, plugin_job, + app->cancellable, + (GAsyncReadyCallback) offline_update_cb, + app); +} + +static void +offline_update_cb (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsApplication *app) +{ + g_autoptr(GError) error = NULL; + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + g_warning ("Failed to trigger offline update: %s", error->message); + return; + } + + gs_utils_invoke_reboot_async (NULL, reboot_failed_cb, app); +} + +static void +quit_activated (GSimpleAction *action, + GVariant *parameter, + gpointer app) +{ + GApplicationFlags flags; + GList *windows; + GtkWidget *window; + + flags = g_application_get_flags (app); + + if (flags & G_APPLICATION_IS_SERVICE) { + windows = gtk_application_get_windows (GTK_APPLICATION (app)); + if (windows) { + window = windows->data; + gtk_widget_hide (window); + } + + return; + } + + g_application_quit (G_APPLICATION (app)); +} + +static void +activate_on_shell_loaded_cb (GsActivationHelper *helper) +{ + GsApplication *app = helper->app; + + g_action_activate (G_ACTION (helper->action), helper->action_param); + + g_signal_handlers_disconnect_by_data (app->shell, helper); + gs_activation_helper_free (helper); +} + +static void +set_mode_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *mode; + + gs_application_present_window (app, NULL); + + gs_shell_reset_state (app->shell); + + mode = g_variant_get_string (parameter, NULL); + if (g_strcmp0 (mode, "updates") == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_UPDATES); + } else if (g_strcmp0 (mode, "installed") == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_INSTALLED); + } else if (g_strcmp0 (mode, "moderate") == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_MODERATE); + } else if (g_strcmp0 (mode, "overview") == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_OVERVIEW); + } else if (g_strcmp0 (mode, "updated") == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_UPDATES); + gs_shell_show_installed_updates (app->shell); + } else { + g_warning ("Mode '%s' not recognised", mode); + } +} + +static void +search_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *search; + + gs_application_present_window (app, NULL); + + search = g_variant_get_string (parameter, NULL); + gs_shell_reset_state (app->shell); + gs_shell_show_search (app->shell, search); +} + +static void +_search_launchable_details_cb (GObject *source, GAsyncResult *res, gpointer user_data) +{ + GsApp *a; + GsApplication *app = GS_APPLICATION (user_data); + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + list = gs_plugin_loader_job_process_finish (app->plugin_loader, res, &error); + if (list == NULL) { + g_warning ("failed to find application: %s", error->message); + return; + } + if (gs_app_list_length (list) == 0) { + gs_shell_set_mode (app->shell, GS_SHELL_MODE_OVERVIEW); + gs_shell_show_notification (app->shell, + /* TRANSLATORS: we tried to show an app that did not exist */ + _("Sorry! There are no details for that application.")); + return; + } + a = gs_app_list_index (list, 0); + gs_shell_reset_state (app->shell); + gs_shell_show_app (app->shell, a); +} + +static void +gs_application_app_to_show_created_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsApplication *gs_app = user_data; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + + app = gs_plugin_loader_app_create_finish (GS_PLUGIN_LOADER (source_object), result, &error); + if (app == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("Failed to create application: %s", error->message); + } else { + g_return_if_fail (GS_IS_APPLICATION (gs_app)); + + gs_shell_reset_state (gs_app->shell); + gs_shell_show_app (gs_app->shell, app); + } +} + +static void +details_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *id; + const gchar *search; + + gs_application_present_window (app, NULL); + + g_variant_get (parameter, "(&s&s)", &id, &search); + g_debug ("trying to activate %s:%s for details", id, search); + if (search != NULL && search[0] != '\0') { + gs_shell_reset_state (app->shell); + gs_shell_show_search_result (app->shell, id, search); + } else { + g_autofree gchar *data_id = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppQuery) query = NULL; + const gchar *keywords[] = { id, NULL }; + + data_id = gs_utils_unique_id_compat_convert (id); + if (data_id != NULL) { + gs_plugin_loader_app_create_async (app->plugin_loader, data_id, app->cancellable, + gs_application_app_to_show_created_cb, app); + return; + } + + /* find by launchable */ + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + gs_plugin_loader_job_process_async (app->plugin_loader, plugin_job, + app->cancellable, + _search_launchable_details_cb, + app); + } +} + +static void +details_pkg_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *name; + const gchar *plugin_name; + g_autoptr (GsApp) a = NULL; + + gs_application_present_window (app, NULL); + + g_variant_get (parameter, "(&s&s)", &name, &plugin_name); + a = gs_app_new (NULL); + gs_app_add_source (a, name); + if (strcmp (plugin_name, "") != 0) { + GsPlugin *plugin = gs_plugin_loader_find_plugin (app->plugin_loader, plugin_name); + gs_app_set_management_plugin (a, plugin); + } + + gs_shell_reset_state (app->shell); + gs_shell_show_app (app->shell, a); +} + +static void +details_url_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *url; + g_autoptr (GsApp) a = NULL; + + gs_application_present_window (app, NULL); + + g_variant_get (parameter, "(&s)", &url); + + /* this is only used as a wrapper to transport the URL to + * the gs_shell_change_mode() function -- not in the GsAppList */ + a = gs_app_new (NULL); + gs_app_set_metadata (a, "GnomeSoftware::from-url", url); + gs_shell_reset_state (app->shell); + gs_shell_show_app (app->shell, a); +} + +typedef struct { + GWeakRef gs_app_weakref; + gchar *data_id; + GsShellInteraction interaction; +} InstallActivatedHelper; + +static void +gs_application_app_to_install_created_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + InstallActivatedHelper *helper = user_data; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + + app = gs_plugin_loader_app_create_finish (GS_PLUGIN_LOADER (source_object), result, &error); + if (app == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + g_warning ("Failed to create application '%s': %s", helper->data_id, error->message); + } else { + g_autoptr(GsApplication) gs_app = NULL; + + gs_app = g_weak_ref_get (&helper->gs_app_weakref); + if (gs_app != NULL) { + gs_shell_reset_state (gs_app->shell); + gs_shell_install (gs_app->shell, app, helper->interaction); + } + } + + g_weak_ref_clear (&helper->gs_app_weakref); + g_free (helper->data_id); + g_slice_free (InstallActivatedHelper, helper); +} + +static void +install_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *id; + GsShellInteraction interaction; + InstallActivatedHelper *helper; + g_autoptr (GsApp) a = NULL; + g_autofree gchar *data_id = NULL; + + g_variant_get (parameter, "(&su)", &id, &interaction); + data_id = gs_utils_unique_id_compat_convert (id); + if (data_id == NULL) { + g_warning ("Need to use a valid unique-id: %s", id); + return; + } + + if (interaction == GS_SHELL_INTERACTION_FULL) + gs_application_present_window (app, NULL); + + helper = g_slice_new0 (InstallActivatedHelper); + g_weak_ref_init (&helper->gs_app_weakref, app); + helper->data_id = g_strdup (data_id); + helper->interaction = interaction; + + gs_plugin_loader_app_create_async (app->plugin_loader, data_id, app->cancellable, + gs_application_app_to_install_created_cb, helper); +} + +typedef struct { + GWeakRef gs_application_weakref; + gchar *data_id; +} UninstallActivatedHelper; + +static void +gs_application_app_to_uninstall_created_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + UninstallActivatedHelper *helper = user_data; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + g_autoptr (GsApplication) self = g_weak_ref_get (&helper->gs_application_weakref); + + app = gs_plugin_loader_app_create_finish (GS_PLUGIN_LOADER (source_object), result, &error); + + if (app == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) { + g_warning ("Failed to create application '%s': %s", helper->data_id, error->message); + } + } else { + if (self != NULL) { + gs_shell_reset_state (self->shell); + gs_shell_uninstall (self->shell, app); + } + } + + g_weak_ref_clear (&helper->gs_application_weakref); + g_free (helper->data_id); +} + + +static void +uninstall_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *self = GS_APPLICATION (data); + const gchar *id; + g_autofree gchar *data_id = NULL; + UninstallActivatedHelper *helper; + + g_variant_get (parameter, "&s", &id); + + data_id = gs_utils_unique_id_compat_convert (id); + if (data_id == NULL) { + g_warning ("Need to use a valid unique-id: %s", id); + return; + } + + gs_application_present_window (self, NULL); + + helper = g_slice_new0 (UninstallActivatedHelper); + g_weak_ref_init (&helper->gs_application_weakref, self); + helper->data_id = g_strdup (data_id); + + gs_plugin_loader_app_create_async (self->plugin_loader, data_id, self->cancellable, + gs_application_app_to_uninstall_created_cb, helper); +} + +static GFile * +_copy_file_to_cache (GFile *file_src, GError **error) +{ + g_autoptr(GFile) file_dest = NULL; + g_autofree gchar *cache_dir = NULL; + g_autofree gchar *cache_fn = NULL; + g_autofree gchar *basename = NULL; + + /* get destination location */ + cache_dir = g_dir_make_tmp ("gnome-software-XXXXXX", error); + if (cache_dir == NULL) + return NULL; + basename = g_file_get_basename (file_src); + cache_fn = g_build_filename (cache_dir, basename, NULL); + + /* copy file to cache */ + file_dest = g_file_new_for_path (cache_fn); + if (!g_file_copy (file_src, file_dest, + G_FILE_COPY_OVERWRITE, + NULL, /* cancellable */ + NULL, NULL, /* progress */ + error)) { + return NULL; + } + return g_steal_pointer (&file_dest); +} + +static void +filename_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *filename; + g_autoptr(GFile) file = NULL; + + g_variant_get (parameter, "(&s)", &filename); + + /* this could go away at any moment, so make a local copy */ + if (g_str_has_prefix (filename, "/tmp") || + g_str_has_prefix (filename, "/var/tmp")) { + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file_src = g_file_new_for_path (filename); + file = _copy_file_to_cache (file_src, &error); + if (file == NULL) { + g_warning ("failed to copy file, falling back to %s: %s", + filename, error->message); + file = g_file_new_for_path (filename); + } + } else { + file = g_file_new_for_path (filename); + } + gs_shell_reset_state (app->shell); + gs_shell_show_local_file (app->shell, file); +} + +static void +show_metainfo_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *filename; + g_autoptr(GFile) file = NULL; + + g_variant_get (parameter, "(^&ay)", &filename); + + file = g_file_new_for_path (filename); + + gs_shell_reset_state (app->shell); + gs_shell_show_metainfo (app->shell, file); +} + +static void +launch_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *self = GS_APPLICATION (data); + GsApp *app = NULL; + const gchar *id, *management_plugin_name; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) search_job = NULL; + g_autoptr(GsPluginJob) launch_job = NULL; + g_autoptr(GError) error = NULL; + guint ii, len; + GsPlugin *management_plugin; + g_autoptr(GsAppQuery) query = NULL; + const gchar *keywords[2] = { NULL, }; + + g_variant_get (parameter, "(&s&s)", &id, &management_plugin_name); + + keywords[0] = id; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + search_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + list = gs_plugin_loader_job_process (self->plugin_loader, search_job, self->cancellable, &error); + if (!list) { + g_warning ("Failed to search for application '%s' (from '%s'): %s", id, management_plugin_name, error ? error->message : "Unknown error"); + return; + } + + management_plugin = gs_plugin_loader_find_plugin (self->plugin_loader, management_plugin_name); + + len = gs_app_list_length (list); + for (ii = 0; ii < len && !app; ii++) { + GsApp *list_app = gs_app_list_index (list, ii); + + if (gs_app_is_installed (list_app) && + gs_app_has_management_plugin (list_app, management_plugin)) + app = list_app; + } + + if (!app) { + g_warning ("Did not find application '%s' from '%s'", id, management_plugin_name); + return; + } + + launch_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_LAUNCH, + "app", app, + NULL); + if (!gs_plugin_loader_job_action (self->plugin_loader, launch_job, self->cancellable, &error)) { + g_warning ("Failed to launch app: %s", error->message); + return; + } +} + +static void +show_offline_updates_error (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + + gs_application_present_window (app, NULL); + + gs_shell_reset_state (app->shell); + gs_shell_set_mode (app->shell, GS_SHELL_MODE_UPDATES); + gs_update_monitor_show_error (app->update_monitor, app->main_window); +} + +static void +autoupdate_activated (GSimpleAction *action, GVariant *parameter, gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + gs_shell_reset_state (app->shell); + gs_shell_set_mode (app->shell, GS_SHELL_MODE_UPDATES); + gs_update_monitor_autoupdate (app->update_monitor); +} + +static void +install_resources_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *mode; + const gchar *startup_id; + const gchar *desktop_id; + const gchar *ident; + g_autofree gchar **resources = NULL; + + g_variant_get (parameter, "(&s^a&s&s&s&s)", &mode, &resources, &startup_id, &desktop_id, &ident); + + gs_application_present_window (app, startup_id); + + gs_shell_reset_state (app->shell); + gs_shell_show_extras_search (app->shell, mode, resources, desktop_id, ident); +} + +static void +verbose_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + gs_debug_set_verbose (app->debug, TRUE); +} + +static GActionEntry actions[] = { + { "about", about_activated, NULL, NULL, NULL }, + { "quit", quit_activated, NULL, NULL, NULL }, + { "verbose", verbose_activated, NULL, NULL, NULL }, + { "nop", NULL, NULL, NULL } +}; + +static GActionEntry actions_after_loading[] = { + { "reboot-and-install", reboot_and_install, NULL, NULL, NULL }, + { "reboot", reboot_activated, NULL, NULL, NULL }, + { "shutdown", shutdown_activated, NULL, NULL, NULL }, + { "launch", launch_activated, "(ss)", NULL, NULL }, + { "show-offline-update-error", show_offline_updates_error, NULL, NULL, NULL }, + { "autoupdate", autoupdate_activated, NULL, NULL, NULL }, + { "sources", sources_activated, NULL, NULL, NULL }, + { "prefs", prefs_activated, NULL, NULL, NULL }, + { "set-mode", set_mode_activated, "s", NULL, NULL }, + { "search", search_activated, "s", NULL, NULL }, + { "details", details_activated, "(ss)", NULL, NULL }, + { "details-pkg", details_pkg_activated, "(ss)", NULL, NULL }, + { "details-url", details_url_activated, "(s)", NULL, NULL }, + { "install", install_activated, "(su)", NULL, NULL }, + { "uninstall", uninstall_activated, "s", NULL, NULL }, + { "filename", filename_activated, "(s)", NULL, NULL }, + { "install-resources", install_resources_activated, "(sassss)", NULL, NULL }, + { "show-metainfo", show_metainfo_activated, "(ay)", NULL, NULL }, + { "nop", NULL, NULL, NULL } +}; + +static void +gs_application_update_software_sources_presence (GApplication *self) +{ + GsApplication *app = GS_APPLICATION (self); + GSimpleAction *action; + gboolean enable_sources; + + action = G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (self), + "sources")); + enable_sources = g_settings_get_boolean (app->settings, + ENABLE_REPOS_DIALOG_CONF_KEY); + g_simple_action_set_enabled (action, enable_sources); +} + +static void +gs_application_settings_changed_cb (GApplication *self, + const gchar *key, + gpointer data) +{ + if (g_strcmp0 (key, ENABLE_REPOS_DIALOG_CONF_KEY) == 0) { + gs_application_update_software_sources_presence (self); + } +} + +static void +wrapper_action_activated_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + GsApplication *app = GS_APPLICATION (data); + const gchar *action_name = g_action_get_name (G_ACTION (action)); + GAction *real_action = g_action_map_lookup_action (G_ACTION_MAP (app->action_map), + action_name); + + if (app->shell_loaded_handler_id != 0) { + GsActivationHelper *helper = gs_activation_helper_new (app, + G_SIMPLE_ACTION (real_action), + parameter); + + g_signal_connect_swapped (app->shell, "loaded", + G_CALLBACK (activate_on_shell_loaded_cb), helper); + return; + } + + g_action_activate (real_action, parameter); +} + +static void +gs_application_add_wrapper_actions (GApplication *application) +{ + GsApplication *app = GS_APPLICATION (application); + GActionMap *map = NULL; + + app->action_map = g_simple_action_group_new (); + map = G_ACTION_MAP (app->action_map); + + /* add the real actions to a different map and add wrapper actions to the + * application instead; the wrapper actions will call the real ones but + * after the "loading state" has finished */ + + g_action_map_add_action_entries (G_ACTION_MAP (map), actions_after_loading, + G_N_ELEMENTS (actions_after_loading), + application); + + for (guint i = 0; i < G_N_ELEMENTS (actions_after_loading); ++i) { + const GActionEntry *entry = &actions_after_loading[i]; + GAction *action = g_action_map_lookup_action (map, entry->name); + g_autoptr (GSimpleAction) simple_action = NULL; + + simple_action = g_simple_action_new (g_action_get_name (action), + g_action_get_parameter_type (action)); + g_signal_connect (simple_action, "activate", + G_CALLBACK (wrapper_action_activated_cb), + application); + g_object_bind_property (simple_action, "enabled", action, + "enabled", G_BINDING_DEFAULT); + g_action_map_add_action (G_ACTION_MAP (application), + G_ACTION (simple_action)); + } +} + +static void startup_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_application_startup (GApplication *application) +{ + GSettings *settings; + GsApplication *app = GS_APPLICATION (application); + g_auto(GStrv) plugin_blocklist = NULL; + g_auto(GStrv) plugin_allowlist = NULL; + const gchar *tmp; + g_autoptr(GAsyncResult) setup_result = NULL; + + G_APPLICATION_CLASS (gs_application_parent_class)->startup (application); + + gs_application_add_wrapper_actions (application); + + g_action_map_add_action_entries (G_ACTION_MAP (application), + actions, G_N_ELEMENTS (actions), + application); + + /* allow for debugging */ + tmp = g_getenv ("GNOME_SOFTWARE_PLUGINS_BLOCKLIST"); + if (tmp != NULL) + plugin_blocklist = g_strsplit (tmp, ",", -1); + tmp = g_getenv ("GNOME_SOFTWARE_PLUGINS_ALLOWLIST"); + if (tmp != NULL) + plugin_allowlist = g_strsplit (tmp, ",", -1); + + app->plugin_loader = gs_plugin_loader_new (g_application_get_dbus_connection (application), NULL); + if (g_file_test (LOCALPLUGINDIR, G_FILE_TEST_EXISTS)) + gs_plugin_loader_add_location (app->plugin_loader, LOCALPLUGINDIR); + + gs_shell_search_provider_setup (app->search_provider, app->plugin_loader); + +#ifdef HAVE_PACKAGEKIT + app->dbus_helper = gs_dbus_helper_new (g_application_get_dbus_connection (application)); +#endif + settings = g_settings_new ("org.gnome.software"); + app->settings = settings; + g_signal_connect_swapped (settings, "changed", + G_CALLBACK (gs_application_settings_changed_cb), + application); + + /* setup UI */ + app->shell = gs_shell_new (); + app->cancellable = g_cancellable_new (); + + app->shell_loaded_handler_id = g_signal_connect (app->shell, "loaded", + G_CALLBACK (gs_application_shell_loaded_cb), + app); + + app->main_window = GTK_WINDOW (app->shell); + gtk_application_add_window (GTK_APPLICATION (app), app->main_window); + + gs_application_update_software_sources_presence (application); + + /* Remove possibly obsolete notifications */ + g_application_withdraw_notification (application, "installed"); + g_application_withdraw_notification (application, "restart-required"); + g_application_withdraw_notification (application, "updates-available"); + g_application_withdraw_notification (application, "updates-installed"); + g_application_withdraw_notification (application, "upgrades-available"); + g_application_withdraw_notification (application, "offline-updates"); + g_application_withdraw_notification (application, "eol"); + + /* Set up the plugins. */ + gs_plugin_loader_setup_async (app->plugin_loader, + (const gchar * const *) plugin_allowlist, + (const gchar * const *) plugin_blocklist, + NULL, + startup_cb, + app); +} + +static void +startup_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsApplication *app = GS_APPLICATION (user_data); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GError) local_error = NULL; + + if (!gs_plugin_loader_setup_finish (plugin_loader, + result, + &local_error)) { + g_warning ("Failed to setup plugins: %s", local_error->message); + exit (1); + } + + /* show the priority of each plugin */ + gs_plugin_loader_dump_state (plugin_loader); + + app->update_monitor = gs_update_monitor_new (app, app->plugin_loader); + + /* Setup the shell only after the plugin loader finished its setup, + thus all plugins are loaded and ready for the jobs. */ + gs_shell_setup (app->shell, app->plugin_loader, app->cancellable); +} + +static void +gs_application_activate (GApplication *application) +{ + GsApplication *app = GS_APPLICATION (application); + + if (app->shell_loaded_handler_id == 0) + gs_shell_set_mode (app->shell, GS_SHELL_MODE_OVERVIEW); + + gs_shell_activate (GS_APPLICATION (application)->shell); +} + +static void +gs_application_constructed (GObject *object) +{ + GsApplication *self = GS_APPLICATION (object); + + G_OBJECT_CLASS (gs_application_parent_class)->constructed (object); + + /* This is needed when the the application's ID isn't + * org.gnome.Software, e.g. for the development profile (when + * `BUILD_PROFILE` is defined). Without this, icon resources can't + * be loaded appropriately. */ + g_application_set_resource_base_path (G_APPLICATION (self), + "/org/gnome/Software"); + + /* Check on our construct-only properties */ + g_assert (self->debug != NULL); +} + +static void +gs_application_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsApplication *self = GS_APPLICATION (object); + + switch ((GsApplicationProperty) prop_id) { + case PROP_DEBUG: + g_value_set_object (value, self->debug); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_application_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsApplication *self = GS_APPLICATION (object); + + switch ((GsApplicationProperty) prop_id) { + case PROP_DEBUG: + /* Construct only */ + g_assert (self->debug == NULL); + self->debug = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_application_dispose (GObject *object) +{ + GsApplication *app = GS_APPLICATION (object); + + g_clear_object (&app->search_provider); + g_clear_object (&app->plugin_loader); + g_clear_object (&app->update_monitor); +#ifdef HAVE_PACKAGEKIT + g_clear_object (&app->dbus_helper); +#endif + g_clear_object (&app->settings); + g_clear_object (&app->action_map); + g_clear_object (&app->debug); + g_clear_pointer (&app->withdraw_notifications, g_hash_table_unref); + + G_OBJECT_CLASS (gs_application_parent_class)->dispose (object); +} + +static GsShellInteraction +get_page_interaction_from_string (const gchar *interaction) +{ + if (g_strcmp0 (interaction, "notify") == 0) + return GS_SHELL_INTERACTION_NOTIFY; + else if (g_strcmp0 (interaction, "none") == 0) + return GS_SHELL_INTERACTION_NONE; + return GS_SHELL_INTERACTION_FULL; +} + +static int +gs_application_handle_local_options (GApplication *app, GVariantDict *options) +{ + GsApplication *self = GS_APPLICATION (app); + const gchar *id; + const gchar *pkgname; + const gchar *local_filename; + const gchar *mode; + const gchar *search; + gint rc = -1; + g_autoptr(GError) error = NULL; + + gs_debug_set_verbose (self->debug, g_variant_dict_contains (options, "verbose")); + + /* prefer local sources */ + if (g_variant_dict_contains (options, "prefer-local")) + g_setenv ("GNOME_SOFTWARE_PREFER_LOCAL", "true", TRUE); + + if (g_variant_dict_contains (options, "version")) { + g_print ("gnome-software %s\n", get_version()); + return 0; + } + + if (!g_application_register (app, NULL, &error)) { + g_printerr ("%s\n", error->message); + return 1; + } + + if (g_variant_dict_contains (options, "autoupdate")) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "autoupdate", + NULL); + } + if (g_variant_dict_contains (options, "prefs")) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "prefs", + NULL); + } + if (g_variant_dict_contains (options, "quit")) { + /* The 'quit' command-line option shuts down everything, + * including the backend service */ + g_action_group_activate_action (G_ACTION_GROUP (app), + "shutdown", + NULL); + return 0; + } + + if (g_variant_dict_contains (options, "verbose")) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "verbose", + NULL); + } + + if (g_variant_dict_lookup (options, "mode", "&s", &mode)) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "set-mode", + g_variant_new_string (mode)); + rc = 0; + } else if (g_variant_dict_lookup (options, "search", "&s", &search)) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "search", + g_variant_new_string (search)); + rc = 0; + } else if (g_variant_dict_lookup (options, "details", "&s", &id)) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "details", + g_variant_new ("(ss)", id, "")); + rc = 0; + } else if (g_variant_dict_lookup (options, "details-pkg", "&s", &pkgname)) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "details-pkg", + g_variant_new ("(ss)", pkgname, "")); + rc = 0; + } else if (g_variant_dict_lookup (options, "install", "&s", &id)) { + GsShellInteraction interaction = GS_SHELL_INTERACTION_FULL; + const gchar *str_interaction = NULL; + + if (g_variant_dict_lookup (options, "interaction", "&s", + &str_interaction)) + interaction = get_page_interaction_from_string (str_interaction); + + g_action_group_activate_action (G_ACTION_GROUP (app), + "install", + g_variant_new ("(su)", id, + interaction)); + rc = 0; + } else if (g_variant_dict_lookup (options, "uninstall", "&s", &id)) { + g_action_group_activate_action (G_ACTION_GROUP (app), + "uninstall", + g_variant_new_string (id)); + rc = 0; + } else if (g_variant_dict_lookup (options, "local-filename", "^&ay", &local_filename)) { + g_autoptr(GFile) file = NULL; + g_autofree gchar *absolute_filename = NULL; + + file = g_file_new_for_path (local_filename); + absolute_filename = g_file_get_path (file); + g_action_group_activate_action (G_ACTION_GROUP (app), + "filename", + g_variant_new ("(s)", absolute_filename)); + rc = 0; + } else if (g_variant_dict_lookup (options, "show-metainfo", "^&ay", &local_filename)) { + g_autoptr(GFile) file = NULL; + g_autofree gchar *absolute_filename = NULL; + + file = g_file_new_for_path (local_filename); + absolute_filename = g_file_get_path (file); + g_action_group_activate_action (G_ACTION_GROUP (app), + "show-metainfo", + g_variant_new ("(^ay)", absolute_filename)); + rc = 0; + } + + return rc; +} + +static void +gs_application_open (GApplication *application, + GFile **files, + gint n_files, + const gchar *hint) +{ + GsApplication *app = GS_APPLICATION (application); + gint i; + + for (i = 0; i < n_files; i++) { + g_autofree gchar *str = g_file_get_uri (files[i]); + g_action_group_activate_action (G_ACTION_GROUP (app), + "details-url", + g_variant_new ("(s)", str)); + } +} + +static void +gs_application_class_init (GsApplicationClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GApplicationClass *application_class = G_APPLICATION_CLASS (klass); + + object_class->constructed = gs_application_constructed; + object_class->get_property = gs_application_get_property; + object_class->set_property = gs_application_set_property; + object_class->dispose = gs_application_dispose; + + application_class->startup = gs_application_startup; + application_class->activate = gs_application_activate; + application_class->handle_local_options = gs_application_handle_local_options; + application_class->open = gs_application_open; + application_class->dbus_register = gs_application_dbus_register; + application_class->dbus_unregister = gs_application_dbus_unregister; + application_class->shutdown = gs_application_shutdown; + + /** + * GsApplication:debug: (nullable) + * + * A #GsDebug object to control debug and logging output from the + * application and everything within it. + * + * This may be %NULL if you don’t care about log output. + * + * Since: 40 + */ + obj_props[PROP_DEBUG] = + g_param_spec_object ("debug", NULL, NULL, + GS_TYPE_DEBUG, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props); + + /** + * GsApplication::install-resources-done: + * @ident: Operation identificator, as string + * @op_error: (nullable): an install #GError, or %NULL on success + * + * Emitted after a resource installation operation identified by @ident + * had finished. The @op_error can hold eventual error message, when + * the installation failed. + */ + signals[INSTALL_RESOURCES_DONE] = g_signal_new ( + "install-resources-done", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + G_TYPE_STRING, G_TYPE_ERROR); + + /** + * GsApplication::repository-changed: + * @repository: a #GsApp of the repository + * + * Emitted when the repository changed, usually when it is enabled or disabled. + * + * Since: 40 + */ + signals[REPOSITORY_CHANGED] = g_signal_new ( + "repository-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_ACTION | G_SIGNAL_NO_RECURSE, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 1, + GS_TYPE_APP); +} + +/** + * gs_application_new: + * @debug: (transfer none) (not nullable): a #GsDebug for the application instance + * + * Create a new #GsApplication. + * + * Returns: (transfer full): a new #GsApplication + * Since: 40 + */ +GsApplication * +gs_application_new (GsDebug *debug) +{ + return g_object_new (GS_APPLICATION_TYPE, + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_HANDLES_OPEN, + "inactivity-timeout", 12000, + "debug", debug, + NULL); +} + +void +gs_application_emit_install_resources_done (GsApplication *application, + const gchar *ident, + const GError *op_error) +{ + g_signal_emit (application, signals[INSTALL_RESOURCES_DONE], 0, ident, op_error, NULL); +} + +static gboolean +gs_application_withdraw_notification_cb (gpointer user_data) +{ + GApplication *application = g_application_get_default (); + const gchar *notification_id = user_data; + + gs_application_withdraw_notification (GS_APPLICATION (application), notification_id); + + return G_SOURCE_REMOVE; +} + +/** + * gs_application_send_notification: + * @self: a #GsApplication + * @notification_id: the @notification ID + * @notification: a #GNotification + * @timeout_minutes: how many minutes to wait, before withdraw the notification; 0 for not withdraw + * + * Sends the @notification and schedules withdraw of it after + * @timeout_minutes. This is used to auto-hide notifications + * after certain period of time. The @timeout_minutes set to 0 + * means to not auto-withdraw it. + * + * Since: 43 + **/ +void +gs_application_send_notification (GsApplication *self, + const gchar *notification_id, + GNotification *notification, + guint timeout_minutes) +{ + guint timeout_id; + + g_return_if_fail (GS_IS_APPLICATION (self)); + g_return_if_fail (notification_id != NULL); + g_return_if_fail (G_IS_NOTIFICATION (notification)); + g_return_if_fail (timeout_minutes < G_MAXUINT / 60); + + g_application_send_notification (G_APPLICATION (self), notification_id, notification); + + if (timeout_minutes > 0) { + if (self->withdraw_notifications == NULL) + self->withdraw_notifications = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + timeout_id = GPOINTER_TO_UINT (g_hash_table_lookup (self->withdraw_notifications, notification_id)); + if (timeout_id) + g_source_remove (timeout_id); + timeout_id = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT, timeout_minutes * 60, + gs_application_withdraw_notification_cb, g_strdup (notification_id), g_free); + g_hash_table_insert (self->withdraw_notifications, g_strdup (notification_id), GUINT_TO_POINTER (timeout_id)); + } else if (self->withdraw_notifications != NULL) { + timeout_id = GPOINTER_TO_UINT (g_hash_table_lookup (self->withdraw_notifications, notification_id)); + if (timeout_id) { + g_source_remove (timeout_id); + g_hash_table_remove (self->withdraw_notifications, notification_id); + } + } +} + +/** + * gs_application_withdraw_notification: + * @self: a #GsApplication + * @notification_id: a #GNotification ID + * + * Immediately withdraws the notification @notification_id and + * removes any previously scheduled withdraw by gs_application_schedule_withdraw_notification(). + * + * Since: 43 + **/ +void +gs_application_withdraw_notification (GsApplication *self, + const gchar *notification_id) +{ + g_return_if_fail (GS_IS_APPLICATION (self)); + g_return_if_fail (notification_id != NULL); + + g_application_withdraw_notification (G_APPLICATION (self), notification_id); + + if (self->withdraw_notifications != NULL) { + g_hash_table_remove (self->withdraw_notifications, notification_id); + if (g_hash_table_size (self->withdraw_notifications) == 0) + g_clear_pointer (&self->withdraw_notifications, g_hash_table_unref); + } +} -- cgit v1.2.3