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-common.c | 1261 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1261 insertions(+) create mode 100644 src/gs-common.c (limited to 'src/gs-common.c') diff --git a/src/gs-common.c b/src/gs-common.c new file mode 100644 index 0000000..7499d74 --- /dev/null +++ b/src/gs-common.c @@ -0,0 +1,1261 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2015 Richard Hughes + * Copyright (C) 2016-2019 Kalev Lember + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include +#include + +#ifndef TESTDATADIR +#include "gs-application.h" +#endif + +#include "gs-common.h" + +#ifdef HAVE_GSETTINGS_DESKTOP_SCHEMAS +#include +#endif + +#include + +void +gs_widget_remove_all (GtkWidget *container, + GsRemoveFunc remove_func) +{ + GtkWidget *child; + while ((child = gtk_widget_get_first_child (container)) != NULL) { + if (remove_func) + remove_func (container, child); + else + gtk_widget_unparent (child); + } +} + +static void +grab_focus (GtkWidget *widget) +{ + g_signal_handlers_disconnect_by_func (widget, grab_focus, NULL); + gtk_widget_grab_focus (widget); +} + +void +gs_grab_focus_when_mapped (GtkWidget *widget) +{ + if (gtk_widget_get_mapped (widget)) + gtk_widget_grab_focus (widget); + else + g_signal_connect_after (widget, "map", + G_CALLBACK (grab_focus), NULL); +} + +void +gs_app_notify_installed (GsApp *app) +{ + g_autofree gchar *summary = NULL; + const gchar *body = NULL; + g_autoptr(GNotification) n = NULL; + + switch (gs_app_get_kind (app)) { + case AS_COMPONENT_KIND_DESKTOP_APP: + /* TRANSLATORS: this is the summary of a notification that an application + * has been successfully installed */ + summary = g_strdup_printf (_("%s is now installed"), gs_app_get_name (app)); + if (gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT)) { + /* TRANSLATORS: an application has been installed, but + * needs a reboot to complete the installation */ + body = _("A restart is required for the changes to take effect."); + } else { + /* TRANSLATORS: this is the body of a notification that an application + * has been successfully installed */ + body = _("Application is ready to be used."); + } + break; + default: + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_GENERIC && + gs_app_get_special_kind (app) == GS_APP_SPECIAL_KIND_OS_UPDATE) { + /* TRANSLATORS: this is the summary of a notification that OS updates + * have been successfully installed */ + summary = g_strdup (_("System updates are now installed")); + /* TRANSLATORS: this is the body of a notification that OS updates + * have been successfully installed */ + body = _("Recently installed updates are available to review"); + } else { + /* TRANSLATORS: this is the summary of a notification that a component + * has been successfully installed */ + summary = g_strdup_printf (_("%s is now installed"), gs_app_get_name (app)); + if (gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT)) { + /* TRANSLATORS: an application has been installed, but + * needs a reboot to complete the installation */ + body = _("A restart is required for the changes to take effect."); + } + } + break; + } + n = g_notification_new (summary); + if (body != NULL) + g_notification_set_body (n, body); + + if (gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT)) { + /* TRANSLATORS: button text */ + g_notification_add_button_with_target (n, _("Restart"), + "app.reboot", NULL); + } else if (gs_app_get_kind (app) == AS_COMPONENT_KIND_DESKTOP_APP) { + /* TRANSLATORS: this is button that opens the newly installed application */ + g_autoptr(GsPlugin) plugin = gs_app_dup_management_plugin (app); + const gchar *plugin_name = (plugin != NULL) ? gs_plugin_get_name (plugin) : ""; + g_notification_add_button_with_target (n, _("Launch"), + "app.launch", "(ss)", + gs_app_get_id (app), + plugin_name); + } + g_notification_set_default_action_and_target (n, "app.details", "(ss)", + gs_app_get_unique_id (app), ""); + #ifdef TESTDATADIR + g_application_send_notification (g_application_get_default (), "installed", n); + #else + gs_application_send_notification (GS_APPLICATION (g_application_get_default ()), "installed", n, 24 * 60); + #endif +} + +typedef enum { + GS_APP_LICENSE_FREE = 0, + GS_APP_LICENSE_NONFREE = 1, + GS_APP_LICENSE_PATENT_CONCERN = 2 +} GsAppLicenseHint; + +typedef struct +{ + gint response_id; + GMainLoop *loop; +} RunInfo; + +static void +shutdown_loop (RunInfo *run_info) +{ + if (g_main_loop_is_running (run_info->loop)) + g_main_loop_quit (run_info->loop); +} + +static void +unmap_cb (GtkDialog *dialog, + RunInfo *run_info) +{ + shutdown_loop (run_info); +} + +static void +response_cb (GtkDialog *dialog, + gint response_id, + RunInfo *run_info) +{ + run_info->response_id = response_id; + gtk_window_destroy (GTK_WINDOW (dialog)); + shutdown_loop (run_info); +} + +static gboolean +close_requested_cb (GtkDialog *dialog, + RunInfo *run_info) +{ + shutdown_loop (run_info); + return GDK_EVENT_PROPAGATE; +} + +GtkResponseType +gs_app_notify_unavailable (GsApp *app, GtkWindow *parent) +{ + GsAppLicenseHint hint = GS_APP_LICENSE_FREE; + GtkWidget *dialog; + const gchar *license; + gboolean already_enabled = FALSE; /* FIXME */ + g_autofree gchar *origin_ui = NULL; + guint i; + struct { + const gchar *str; + GsAppLicenseHint hint; + } keywords[] = { + { "NonFree", GS_APP_LICENSE_NONFREE }, + { "PatentConcern", GS_APP_LICENSE_PATENT_CONCERN }, + { "Proprietary", GS_APP_LICENSE_NONFREE }, + { NULL, 0 } + }; + g_autoptr(GSettings) settings = NULL; + g_autoptr(GString) body = NULL; + g_autoptr(GString) title = NULL; + + RunInfo run_info = { + GTK_RESPONSE_NONE, + NULL, + }; + + /* this is very crude */ + license = gs_app_get_license (app); + if (license != NULL) { + for (i = 0; keywords[i].str != NULL; i++) { + if (g_strstr_len (license, -1, keywords[i].str) != NULL) + hint |= keywords[i].hint; + } + } else { + /* use the worst-case assumption */ + hint = GS_APP_LICENSE_NONFREE | GS_APP_LICENSE_PATENT_CONCERN; + } + + /* check if the user has already dismissed */ + settings = g_settings_new ("org.gnome.software"); + if (!g_settings_get_boolean (settings, "prompt-for-nonfree")) + return GTK_RESPONSE_OK; + + title = g_string_new (""); + if (already_enabled) { + g_string_append_printf (title, "%s", + /* TRANSLATORS: window title */ + _("Install Third-Party Software?")); + } else { + g_string_append_printf (title, "%s", + /* TRANSLATORS: window title */ + _("Enable Third-Party Software Repository?")); + } + dialog = gtk_message_dialog_new (parent, + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_CANCEL, + NULL); + gtk_message_dialog_set_markup (GTK_MESSAGE_DIALOG (dialog), title->str); + + body = g_string_new (""); + origin_ui = gs_app_dup_origin_ui (app, TRUE); + + if (hint & GS_APP_LICENSE_NONFREE) { + g_string_append_printf (body, + /* TRANSLATORS: the replacements are as follows: + * 1. Application name, e.g. "Firefox" + * 2. Software repository name, e.g. fedora-optional + */ + _("%s is not " + "free and open source software, " + "and is provided by “%s”."), + gs_app_get_name (app), + origin_ui); + } else { + g_string_append_printf (body, + /* TRANSLATORS: the replacements are as follows: + * 1. Application name, e.g. "Firefox" + * 2. Software repository name, e.g. fedora-optional */ + _("%s is provided by “%s”."), + gs_app_get_name (app), + origin_ui); + } + + /* tell the use what needs to be done */ + if (!already_enabled) { + g_string_append (body, " "); + g_string_append (body, + _("This software repository must be " + "enabled to continue installation.")); + } + + /* be aware of patent clauses */ + if (hint & GS_APP_LICENSE_PATENT_CONCERN) { + g_string_append (body, "\n\n"); + if (gs_app_get_kind (app) != AS_COMPONENT_KIND_CODEC) { + g_string_append_printf (body, + /* TRANSLATORS: Laws are geographical, urgh... */ + _("It may be illegal to install " + "or use %s in some countries."), + gs_app_get_name (app)); + } else { + g_string_append (body, + /* TRANSLATORS: Laws are geographical, urgh... */ + _("It may be illegal to install or use " + "this codec in some countries.")); + } + } + + gtk_message_dialog_format_secondary_markup (GTK_MESSAGE_DIALOG (dialog), "%s", body->str); + /* TRANSLATORS: this is button text to not ask about non-free content again */ + if (0) gtk_dialog_add_button (GTK_DIALOG (dialog), _("Don’t Warn Again"), GTK_RESPONSE_YES); + if (already_enabled) { + gtk_dialog_add_button (GTK_DIALOG (dialog), + /* TRANSLATORS: button text */ + _("Install"), + GTK_RESPONSE_OK); + } else { + gtk_dialog_add_button (GTK_DIALOG (dialog), + /* TRANSLATORS: button text */ + _("Enable and Install"), + GTK_RESPONSE_OK); + } + + + /* Run */ + if (!gtk_widget_get_visible (dialog)) + gtk_window_present (GTK_WINDOW (dialog)); + + g_signal_connect (dialog, "close-request", G_CALLBACK (close_requested_cb), &run_info); + g_signal_connect (dialog, "response", G_CALLBACK (response_cb), &run_info); + g_signal_connect (dialog, "unmap", G_CALLBACK (unmap_cb), &run_info); + + run_info.loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (run_info.loop); + g_clear_pointer (&run_info.loop, g_main_loop_unref); + + if (run_info.response_id == GTK_RESPONSE_YES) { + run_info.response_id = GTK_RESPONSE_OK; + g_settings_set_boolean (settings, "prompt-for-nonfree", FALSE); + } + return run_info.response_id; +} + +gboolean +gs_utils_is_current_desktop (const gchar *name) +{ + const gchar *tmp; + g_auto(GStrv) names = NULL; + tmp = g_getenv ("XDG_CURRENT_DESKTOP"); + if (tmp == NULL) + return FALSE; + names = g_strsplit (tmp, ":", -1); + return g_strv_contains ((const gchar * const *) names, name); +} + +static void +gs_utils_widget_css_parsing_error_cb (GtkCssProvider *provider, + GtkCssSection *section, + GError *error, + gpointer user_data) +{ + const GtkCssLocation *start_location; + + start_location = gtk_css_section_get_start_location (section); + g_warning ("CSS parse error %" G_GSIZE_FORMAT ":%" G_GSIZE_FORMAT ": %s", + start_location->lines + 1, + start_location->line_chars, + error->message); +} + +/** + * gs_utils_set_key_colors_in_css: + * @css: some CSS + * @app: a #GsApp to get the key colors from + * + * Replace placeholders in @css with the key colors from @app, returning a copy + * of the CSS with the key colors inlined as `rgb()` literals. + * + * The key color placeholders are of the form `@keycolor-XX@`, where `XX` is a + * two digit counter. The first counter (`00`) will be replaced with the first + * key color in @app, the second counter (`01`) with the second, etc. + * + * CSS may be %NULL, in which case %NULL is returned. + * + * Returns: (transfer full): a copy of @css with the key color placeholders + * replaced, free with g_free() + * Since: 40 + */ +gchar * +gs_utils_set_key_colors_in_css (const gchar *css, + GsApp *app) +{ + GArray *key_colors; + g_autoptr(GString) css_new = NULL; + + if (css == NULL) + return NULL; + + key_colors = gs_app_get_key_colors (app); + + /* Do we not need to do any replacements? */ + if (key_colors->len == 0 || + g_strstr_len (css, -1, "@keycolor") == NULL) + return g_strdup (css); + + /* replace key color values */ + css_new = g_string_new (css); + for (guint j = 0; j < key_colors->len; j++) { + const GdkRGBA *color = &g_array_index (key_colors, GdkRGBA, j); + g_autofree gchar *key = NULL; + g_autofree gchar *value = NULL; + key = g_strdup_printf ("@keycolor-%02u@", j); + value = g_strdup_printf ("rgb(%.0f,%.0f,%.0f)", + color->red * 255.f, + color->green * 255.f, + color->blue * 255.f); + as_gstring_replace (css_new, key, value); + } + + return g_string_free (g_steal_pointer (&css_new), FALSE); +} + +/** + * gs_utils_widget_set_css: + * @widget: a widget + * @provider: (inout) (transfer full) (not optional) (nullable): pointer to a + * #GtkCssProvider to use + * @class_name: class name to use, without the leading `.` + * @css: (nullable): CSS to set on the widget, or %NULL to clear custom CSS + * + * Set custom CSS on the given @widget instance. This doesn’t affect any other + * instances of the same widget. The @class_name must be a static string to be + * used as a name for the @css. It doesn’t need to vary with @widget, but + * multiple values of @class_name can be used with the same @widget to control + * several independent snippets of custom CSS. + * + * @provider must be a pointer to a #GtkCssProvider pointer, typically within + * your widget’s private data struct. This function will return a + * #GtkCssProvider in the provided pointer, reusing any old @provider if + * possible. When your widget is destroyed, you must destroy the returned + * @provider. If @css is %NULL, this function will destroy the @provider. + */ +void +gs_utils_widget_set_css (GtkWidget *widget, GtkCssProvider **provider, const gchar *class_name, const gchar *css) +{ + GtkStyleContext *context; + g_autoptr(GString) str = NULL; + + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (provider != NULL); + g_return_if_fail (provider == NULL || *provider == NULL || GTK_IS_STYLE_PROVIDER (*provider)); + g_return_if_fail (class_name != NULL); + + context = gtk_widget_get_style_context (widget); + + /* remove custom class if NULL */ + if (css == NULL) { + if (*provider != NULL) + gtk_style_context_remove_provider (context, GTK_STYLE_PROVIDER (*provider)); + g_clear_object (provider); + gtk_style_context_remove_class (context, class_name); + return; + } + + str = g_string_sized_new (1024); + g_string_append_printf (str, ".%s {\n", class_name); + g_string_append_printf (str, "%s\n", css); + g_string_append (str, "}"); + + /* create a new provider if needed */ + if (*provider == NULL) { + *provider = gtk_css_provider_new (); + g_signal_connect (*provider, "parsing-error", + G_CALLBACK (gs_utils_widget_css_parsing_error_cb), NULL); + } + + /* set the custom CSS class */ + gtk_style_context_add_class (context, class_name); + + /* set up custom provider and store on the widget */ + gtk_css_provider_load_from_data (*provider, str->str, -1); + gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (*provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +static void +unset_focus (GtkWidget *widget, gpointer data) +{ + if (GTK_IS_WINDOW (widget)) + gtk_window_set_focus (GTK_WINDOW (widget), NULL); +} + +/** + * insert_details_widget: + * @dialog: the message dialog where the widget will be inserted + * @details: the detailed message text to display + * + * Inserts a widget displaying the detailed message into the message dialog. + */ +static void +insert_details_widget (GtkMessageDialog *dialog, + const gchar *details, + gboolean add_prefix) +{ + GtkWidget *message_area, *sw, *label; + GtkWidget *tv; + GtkWidget *child; + GtkTextBuffer *buffer; + g_autoptr(GString) msg = NULL; + + g_assert (GTK_IS_MESSAGE_DIALOG (dialog)); + g_assert (details != NULL); + + gtk_window_set_resizable (GTK_WINDOW (dialog), TRUE); + + if (add_prefix) { + msg = g_string_new (""); + g_string_append_printf (msg, "%s\n\n%s", + /* TRANSLATORS: these are show_detailed_error messages from the + * package manager no mortal is supposed to understand, + * but google might know what they mean */ + _("Detailed errors from the package manager follow:"), + details); + } + + message_area = gtk_message_dialog_get_message_area (dialog); + g_assert (GTK_IS_BOX (message_area)); + + /* Find the secondary label and set its width_chars. */ + /* Otherwise the label will tend to expand vertically. */ + child = gtk_widget_get_first_child (message_area); + if (child) { + GtkWidget *next = gtk_widget_get_next_sibling (child); + if (next && GTK_IS_LABEL (next)) + gtk_label_set_width_chars (GTK_LABEL (next), 40); + } + + label = gtk_label_new (_("Details")); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_widget_set_visible (label, TRUE); + gtk_box_append (GTK_BOX (message_area), label); + + sw = gtk_scrolled_window_new (); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (sw), 150); + gtk_widget_set_visible (sw, TRUE); + + tv = gtk_text_view_new (); + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv)); + gtk_text_view_set_editable (GTK_TEXT_VIEW (tv), FALSE); + gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD); + gtk_style_context_add_class (gtk_widget_get_style_context (tv), + "update-failed-details"); + gtk_text_buffer_set_text (buffer, msg ? msg->str : details, -1); + gtk_widget_set_visible (tv, TRUE); + + gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), tv); + gtk_widget_set_vexpand (sw, TRUE); + gtk_box_append (GTK_BOX (message_area), sw); + + g_signal_connect (dialog, "map", G_CALLBACK (unset_focus), NULL); +} + +/** + * gs_utils_show_error_dialog: + * @parent: transient parent, or NULL for none + * @title: the title for the dialog + * @msg: the message for the dialog + * @details: (allow-none): the detailed error message, or NULL for none + * + * Shows a message dialog for displaying error messages. + */ +void +gs_utils_show_error_dialog (GtkWindow *parent, + const gchar *title, + const gchar *msg, + const gchar *details) +{ + GtkWidget *dialog; + + dialog = gtk_message_dialog_new_with_markup (parent, + 0, + GTK_MESSAGE_INFO, + GTK_BUTTONS_CLOSE, + "%s", title); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", msg); + if (details != NULL) + insert_details_widget (GTK_MESSAGE_DIALOG (dialog), details, TRUE); + + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_window_destroy), + dialog); + gtk_widget_show (dialog); +} + +/** + * gs_utils_ask_user_accepts: + * @parent: (nullable): modal parent, or %NULL for none + * @title: the title for the dialog + * @msg: the message for the dialog + * @details: (nullable): the detailed error message, or %NULL for none + * @accept_label: (nullable): a label of the 'accept' button, or %NULL to use 'Accept' + * + * Shows a modal question dialog for displaying an accept/cancel question to the user. + * + * Returns: whether the user accepted the question + * + * Since: 42 + **/ +gboolean +gs_utils_ask_user_accepts (GtkWindow *parent, + const gchar *title, + const gchar *msg, + const gchar *details, + const gchar *accept_label) +{ + GtkWidget *dialog; + RunInfo run_info; + + g_return_val_if_fail (parent == NULL || GTK_IS_WINDOW (parent), FALSE); + g_return_val_if_fail (title != NULL, FALSE); + g_return_val_if_fail (msg != NULL, FALSE); + + if (accept_label == NULL || *accept_label == '\0') { + /* Translators: an accept button label, in a Cancel/Accept dialog */ + accept_label = _("_Accept"); + } + + dialog = gtk_message_dialog_new_with_markup (parent, + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + "%s", title); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", msg); + if (details != NULL) + insert_details_widget (GTK_MESSAGE_DIALOG (dialog), details, FALSE); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Cancel"), GTK_RESPONSE_CANCEL); + gtk_dialog_add_button (GTK_DIALOG (dialog), accept_label, GTK_RESPONSE_OK); + + run_info.response_id = GTK_RESPONSE_NONE; + run_info.loop = g_main_loop_new (NULL, FALSE); + + /* Run */ + if (!gtk_widget_get_visible (dialog)) + gtk_window_present (GTK_WINDOW (dialog)); + + g_signal_connect (dialog, "close-request", G_CALLBACK (close_requested_cb), &run_info); + g_signal_connect (dialog, "response", G_CALLBACK (response_cb), &run_info); + g_signal_connect (dialog, "unmap", G_CALLBACK (unmap_cb), &run_info); + + g_main_loop_run (run_info.loop); + g_clear_pointer (&run_info.loop, g_main_loop_unref); + + return run_info.response_id == GTK_RESPONSE_OK; +} + +/** + * gs_utils_get_error_value: + * @error: A GError + * + * Gets the machine-readable value stored in the error message. + * The machine readable string is after the first "@", e.g. + * message = "Requires authentication with @aaa" + * + * Returns: a string, or %NULL + */ +const gchar * +gs_utils_get_error_value (const GError *error) +{ + gchar *str; + if (error == NULL) + return NULL; + str = g_strstr_len (error->message, -1, "@"); + if (str == NULL) + return NULL; + return (const gchar *) str + 1; +} + +/** + * gs_utils_build_unique_id_kind: + * @kind: A #AsComponentKind + * @id: An application ID + * + * Converts the ID valid into a wildcard unique ID of a specific kind. + * If @id is already a unique ID, then it is returned unchanged. + * + * Returns: (transfer full): a unique ID, or %NULL + */ +gchar * +gs_utils_build_unique_id_kind (AsComponentKind kind, const gchar *id) +{ + if (as_utils_data_id_valid (id)) + return g_strdup (id); + return gs_utils_build_unique_id (AS_COMPONENT_SCOPE_UNKNOWN, + AS_BUNDLE_KIND_UNKNOWN, + NULL, + id, + NULL); +} + +/** + * gs_utils_list_has_component_fuzzy: + * @list: A #GsAppList + * @app: A #GsApp + * + * Finds out if any application in the list would match a given application, + * where the match is valid for a matching D-Bus bus name, + * the label in the UI or the same icon. + * + * This function is normally used to work out if the source should be shown + * in a GsAppRow. + * + * Returns: %TRUE if the app is visually the "same" + */ +gboolean +gs_utils_list_has_component_fuzzy (GsAppList *list, GsApp *app) +{ + guint i; + GsApp *tmp; + + for (i = 0; i < gs_app_list_length (list); i++) { + tmp = gs_app_list_index (list, i); + + /* ignore if the same object */ + if (app == tmp) + continue; + + /* ignore with the same source */ + if (g_strcmp0 (gs_app_get_origin_hostname (tmp), + gs_app_get_origin_hostname (app)) == 0) { + continue; + } + + /* same D-Bus ID */ + if (g_strcmp0 (gs_app_get_id (tmp), + gs_app_get_id (app)) == 0) { + return TRUE; + } + + /* same name */ + if (g_strcmp0 (gs_app_get_name (tmp), + gs_app_get_name (app)) == 0) { + return TRUE; + } + } + return FALSE; +} + +void +gs_utils_reboot_notify (GsAppList *list, + gboolean is_install) +{ + g_autoptr(GNotification) n = NULL; + g_autofree gchar *tmp = NULL; + const gchar *app_name = NULL; + const gchar *title; + const gchar *body; + + if (gs_app_list_length (list) == 1) { + GsApp *app = gs_app_list_index (list, 0); + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_DESKTOP_APP) { + app_name = gs_app_get_name (app); + if (!*app_name) + app_name = NULL; + } + } + + if (is_install) { + if (app_name) { + /* TRANSLATORS: The '%s' is replaced with the application name */ + tmp = g_strdup_printf ("An application “%s” has been installed", app_name); + title = tmp; + } else { + /* TRANSLATORS: we've just live-updated some apps */ + title = ngettext ("An update has been installed", + "Updates have been installed", + gs_app_list_length (list)); + } + } else if (app_name) { + /* TRANSLATORS: The '%s' is replaced with the application name */ + tmp = g_strdup_printf ("An application “%s” has been removed", app_name); + title = tmp; + } else { + /* TRANSLATORS: we've just removed some apps */ + title = ngettext ("An application has been removed", + "Applications have been removed", + gs_app_list_length (list)); + } + + /* TRANSLATORS: the new apps will not be run until we restart */ + body = ngettext ("A restart is required for it to take effect.", + "A restart is required for them to take effect.", + gs_app_list_length (list)); + + n = g_notification_new (title); + g_notification_set_body (n, body); + /* TRANSLATORS: button text */ + g_notification_add_button (n, _("Not Now"), "app.nop"); + /* TRANSLATORS: button text */ + g_notification_add_button_with_target (n, _("Restart"), "app.reboot", NULL); + g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates"); + g_notification_set_priority (n, G_NOTIFICATION_PRIORITY_URGENT); + #ifdef TESTDATADIR + g_application_send_notification (g_application_get_default (), "restart-required", n); + #else + gs_application_send_notification (GS_APPLICATION (g_application_get_default ()), "restart-required", n, 0); + #endif +} + +/** + * gs_utils_split_time_difference: + * @unix_time_seconds: Time since the epoch in seconds + * @out_minutes_ago: (out) (nullable): how many minutes elapsed + * @out_hours_ago: (out) (nullable): how many hours elapsed + * @out_days_ago: (out) (nullable): how many days elapsed + * @out_weeks_ago: (out) (nullable): how many weeks elapsed + * @out_months_ago: (out) (nullable): how many months elapsed + * @out_years_ago: (out) (nullable): how many years elapsed + * + * Calculates the difference between the @unix_time_seconds and the current time + * and splits it into separate values. + * + * Returns: whether the out parameters had been set + * + * Since: 41 + **/ +gboolean +gs_utils_split_time_difference (gint64 unix_time_seconds, + gint *out_minutes_ago, + gint *out_hours_ago, + gint *out_days_ago, + gint *out_weeks_ago, + gint *out_months_ago, + gint *out_years_ago) +{ + gint minutes_ago, hours_ago, days_ago; + gint weeks_ago, months_ago, years_ago; + g_autoptr(GDateTime) date_time = NULL; + g_autoptr(GDateTime) now = NULL; + GTimeSpan timespan; + + if (unix_time_seconds <= 0) + return FALSE; + + date_time = g_date_time_new_from_unix_local (unix_time_seconds); + now = g_date_time_new_now_local (); + timespan = g_date_time_difference (now, date_time); + + minutes_ago = (gint) (timespan / G_TIME_SPAN_MINUTE); + hours_ago = (gint) (timespan / G_TIME_SPAN_HOUR); + days_ago = (gint) (timespan / G_TIME_SPAN_DAY); + weeks_ago = days_ago / 7; + months_ago = days_ago / 30; + years_ago = weeks_ago / 52; + + if (out_minutes_ago) + *out_minutes_ago = minutes_ago; + if (out_hours_ago) + *out_hours_ago = hours_ago; + if (out_days_ago) + *out_days_ago = days_ago; + if (out_weeks_ago) + *out_weeks_ago = weeks_ago; + if (out_months_ago) + *out_months_ago = months_ago; + if (out_years_ago) + *out_years_ago = years_ago; + + return TRUE; +} + +/** + * gs_utils_time_to_string: + * @unix_time_seconds: Time since the epoch in seconds + * + * Converts a time to a string such as "5 minutes ago" or "2 weeks ago" + * + * Returns: (transfer full): the time string, or %NULL if @unix_time_seconds is + * not valid + */ +gchar * +gs_utils_time_to_string (gint64 unix_time_seconds) +{ + gint minutes_ago, hours_ago, days_ago; + gint weeks_ago, months_ago, years_ago; + + if (!gs_utils_split_time_difference (unix_time_seconds, + &minutes_ago, &hours_ago, &days_ago, + &weeks_ago, &months_ago, &years_ago)) + return NULL; + + if (minutes_ago < 5) { + /* TRANSLATORS: something happened less than 5 minutes ago */ + return g_strdup (_("Just now")); + } else if (hours_ago < 1) + return g_strdup_printf (ngettext ("%d minute ago", + "%d minutes ago", minutes_ago), + minutes_ago); + else if (days_ago < 1) + return g_strdup_printf (ngettext ("%d hour ago", + "%d hours ago", hours_ago), + hours_ago); + else if (days_ago < 15) + return g_strdup_printf (ngettext ("%d day ago", + "%d days ago", days_ago), + days_ago); + else if (weeks_ago < 8) + return g_strdup_printf (ngettext ("%d week ago", + "%d weeks ago", weeks_ago), + weeks_ago); + else if (years_ago < 1) + return g_strdup_printf (ngettext ("%d month ago", + "%d months ago", months_ago), + months_ago); + else + return g_strdup_printf (ngettext ("%d year ago", + "%d years ago", years_ago), + years_ago); +} + +static void +gs_utils_reboot_call_done_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) local_error = NULL; + + /* get result */ + if (gs_utils_invoke_reboot_finish (source, res, &local_error)) + return; + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Calling reboot had been cancelled"); + else if (local_error != NULL) + g_warning ("Calling reboot failed: %s", local_error->message); +} + +static void +gs_utils_invoke_reboot_ready3_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GVariant) ret_val = NULL; + g_autoptr(GError) local_error = NULL; + + ret_val = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), result, &local_error); + if (ret_val != NULL) { + g_task_return_boolean (task, TRUE); + } else { + const gchar *method_name = g_task_get_task_data (task); + g_dbus_error_strip_remote_error (local_error); + g_prefix_error (&local_error, "Failed to call %s: ", method_name); + g_task_return_error (task, g_steal_pointer (&local_error)); + } +} + +static void +gs_utils_invoke_reboot_ready2_got_session_bus_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GDBusConnection) bus = NULL; + g_autoptr(GError) local_error = NULL; + GCancellable *cancellable; + + bus = g_bus_get_finish (result, &local_error); + if (bus == NULL) { + g_dbus_error_strip_remote_error (local_error); + g_prefix_error_literal (&local_error, "Failed to get D-Bus session bus: "); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + cancellable = g_task_get_cancellable (task); + + /* Make sure file buffers are written to the disk before invoking reboot */ + sync (); + + g_task_set_task_data (task, (gpointer) "org.gnome.SessionManager.Reboot", NULL); + g_dbus_connection_call (bus, + "org.gnome.SessionManager", + "/org/gnome/SessionManager", + "org.gnome.SessionManager", + "Reboot", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, cancellable, + gs_utils_invoke_reboot_ready3_cb, + g_steal_pointer (&task)); +} + +static void +gs_utils_invoke_reboot_ready2_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GVariant) ret_val = NULL; + g_autoptr(GError) local_error = NULL; + + ret_val = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), result, &local_error); + if (ret_val != NULL) { + g_task_return_boolean (task, TRUE); + } else { + g_autoptr(GDBusConnection) bus = NULL; + GCancellable *cancellable; + const gchar *method_name = g_task_get_task_data (task); + + g_dbus_error_strip_remote_error (local_error); + g_prefix_error (&local_error, "Failed to call %s: ", method_name); + + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_debug ("%s", local_error->message); + g_clear_error (&local_error); + + cancellable = g_task_get_cancellable (task); + + g_bus_get (G_BUS_TYPE_SESSION, cancellable, + gs_utils_invoke_reboot_ready2_got_session_bus_cb, + g_steal_pointer (&task)); + } +} + +static void +gs_utils_invoke_reboot_ready1_got_system_bus_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GDBusConnection) bus = NULL; + g_autoptr(GError) local_error = NULL; + GCancellable *cancellable; + + bus = g_bus_get_finish (result, &local_error); + if (bus == NULL) { + g_dbus_error_strip_remote_error (local_error); + g_prefix_error_literal (&local_error, "Failed to get D-Bus system bus: "); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + cancellable = g_task_get_cancellable (task); + + /* Make sure file buffers are written to the disk before invoking reboot */ + sync (); + + g_task_set_task_data (task, (gpointer) "org.freedesktop.login1.Manager.Reboot", NULL); + g_dbus_connection_call (bus, + "org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + "Reboot", + g_variant_new ("(b)", TRUE), /* interactive */ + NULL, G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, cancellable, + gs_utils_invoke_reboot_ready2_cb, + g_steal_pointer (&task)); +} + +static void +gs_utils_invoke_reboot_ready1_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GVariant) ret_val = NULL; + g_autoptr(GError) local_error = NULL; + + ret_val = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), result, &local_error); + if (ret_val != NULL) { + g_task_return_boolean (task, TRUE); + } else { + GCancellable *cancellable; + const gchar *method_name = g_task_get_task_data (task); + + g_dbus_error_strip_remote_error (local_error); + g_prefix_error (&local_error, "Failed to call %s: ", method_name); + + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_debug ("%s", local_error->message); + g_clear_error (&local_error); + + cancellable = g_task_get_cancellable (task); + + g_bus_get (G_BUS_TYPE_SYSTEM, cancellable, + gs_utils_invoke_reboot_ready1_got_system_bus_cb, + g_steal_pointer (&task)); + } +} + +static void +gs_utils_invoke_reboot_got_session_bus_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GDBusConnection) bus = NULL; + g_autoptr(GError) local_error = NULL; + GCancellable *cancellable; + const gchar *xdg_desktop; + gboolean call_session_manager = FALSE; + + bus = g_bus_get_finish (result, &local_error); + if (bus == NULL) { + g_dbus_error_strip_remote_error (local_error); + g_prefix_error_literal (&local_error, "Failed to get D-Bus session bus: "); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* Make sure file buffers are written to the disk before invoking reboot */ + sync (); + + cancellable = g_task_get_cancellable (task); + + xdg_desktop = g_getenv ("XDG_CURRENT_DESKTOP"); + if (xdg_desktop != NULL) { + if (strstr (xdg_desktop, "KDE")) { + g_task_set_task_data (task, (gpointer) "org.kde.Shutdown.logoutAndReboot", NULL); + g_dbus_connection_call (bus, + "org.kde.Shutdown", + "/Shutdown", + "org.kde.Shutdown", + "logoutAndReboot", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, cancellable, + gs_utils_invoke_reboot_ready1_cb, + g_steal_pointer (&task)); + } else if (strstr (xdg_desktop, "LXDE")) { + g_task_set_task_data (task, (gpointer) "org.lxde.SessionManager.RequestReboot", NULL); + g_dbus_connection_call (bus, + "org.lxde.SessionManager", + "/org/lxde/SessionManager", + "org.lxde.SessionManager", + "RequestReboot", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, cancellable, + gs_utils_invoke_reboot_ready1_cb, + g_steal_pointer (&task)); + } else if (strstr (xdg_desktop, "MATE")) { + g_task_set_task_data (task, (gpointer) "org.gnome.SessionManager.RequestReboot", NULL); + g_dbus_connection_call (bus, + "org.gnome.SessionManager", + "/org/gnome/SessionManager", + "org.gnome.SessionManager", + "RequestReboot", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, cancellable, + gs_utils_invoke_reboot_ready1_cb, + g_steal_pointer (&task)); + } else if (strstr (xdg_desktop, "XFCE")) { + g_task_set_task_data (task, (gpointer) "org.xfce.Session.Manager.Restart", NULL); + g_dbus_connection_call (bus, + "org.xfce.SessionManager", + "/org/xfce/SessionManager", + "org.xfce.Session.Manager", + "Restart", + g_variant_new ("(b)", TRUE), /* allow_save */ + NULL, G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, cancellable, + gs_utils_invoke_reboot_ready1_cb, + g_steal_pointer (&task)); + } else { + /* Let the "GNOME" and "X-Cinnamon" be the default */ + call_session_manager = TRUE; + } + } else { + call_session_manager = TRUE; + } + + if (call_session_manager) { + g_task_set_task_data (task, (gpointer) "org.gnome.SessionManager.Reboot", NULL); + g_dbus_connection_call (bus, + "org.gnome.SessionManager", + "/org/gnome/SessionManager", + "org.gnome.SessionManager", + "Reboot", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, cancellable, + gs_utils_invoke_reboot_ready3_cb, + g_steal_pointer (&task)); + } +} + +/** + * gs_utils_invoke_reboot_async: + * @cancellable: (nullable): a %GCancellable for the call, or %NULL + * @ready_callback: (nullable): a callback to be called after the call is finished, or %NULL + * @user_data: user data for the @ready_callback + * + * Asynchronously invokes a reboot request. Finish the operation + * with gs_utils_invoke_reboot_finish(). + * + * When the @ready_callback is %NULL, a default callback is used, which shows + * a runtime warning (using g_warning) on the console when the call fails. + * + * Since: 41 + **/ +void +gs_utils_invoke_reboot_async (GCancellable *cancellable, + GAsyncReadyCallback ready_callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + if (!ready_callback) + ready_callback = gs_utils_reboot_call_done_cb; + + task = g_task_new (NULL, cancellable, ready_callback, user_data); + g_task_set_source_tag (task, gs_utils_invoke_reboot_async); + + g_bus_get (G_BUS_TYPE_SESSION, cancellable, + gs_utils_invoke_reboot_got_session_bus_cb, + g_steal_pointer (&task)); +} + +/** + * gs_utils_invoke_reboot_finish: + * @source_object: the source object provided in the ready callback + * @result: the result object provided in the ready callback + * @error: a #GError, or %NULL + * + * Finishes gs_utils_invoke_reboot_async() call. + * + * Returns: Whether succeeded. If failed, the @error is set. + * + * Since: 43 + **/ +gboolean +gs_utils_invoke_reboot_finish (GObject *source_object, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_task_is_valid (result, source_object), FALSE); + g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gs_utils_invoke_reboot_async, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +/** + * gs_utils_format_size: + * @size_bytes: size to format, in bytes + * @out_is_markup: (out) (not nullable): stores whther the returned string is a markup + * + * Similar to `g_format_size`, only splits the value and the unit into + * separate parts and draws the unit with a smaller font, in case + * the relevant code is available in GLib while compiling. + * + * The @out_is_markup is always set, providing the information about + * used format of the returned string. + * + * Returns: (transfer full): a new string, containing the @size_bytes formatted as string + * + * Since: 43 + **/ +gchar * +gs_utils_format_size (guint64 size_bytes, + gboolean *out_is_markup) +{ +#ifdef HAVE_G_FORMAT_SIZE_ONLY_VALUE + g_autofree gchar *value_str = g_format_size_full (size_bytes, G_FORMAT_SIZE_ONLY_VALUE); + g_autofree gchar *unit_str = g_format_size_full (size_bytes, G_FORMAT_SIZE_ONLY_UNIT); + g_autofree gchar *value_escaped = g_markup_escape_text (value_str, -1); + g_autofree gchar *unit_escaped = g_markup_printf_escaped ("%s", unit_str); + g_return_val_if_fail (out_is_markup != NULL, NULL); + *out_is_markup = TRUE; + /* Translators: This is to construct a disk size string consisting of the value and its unit, while + * the unit is drawn with a smaller font. If you need to flip the order, then you can use "%2$s %1$s". + * Make sure you'll preserve the no break space between the values. + * Example result: "13.0 MB" */ + return g_strdup_printf (C_("format-size", "%s\302\240%s"), value_escaped, unit_escaped); +#else /* HAVE_G_FORMAT_SIZE_ONLY_VALUE */ + g_return_val_if_fail (out_is_markup != NULL, NULL); + *out_is_markup = FALSE; + return g_format_size (size_bytes); +#endif /* HAVE_G_FORMAT_SIZE_ONLY_VALUE */ +} -- cgit v1.2.3