diff pui-application.c @ 0:6884bb8130ca

Initial revision
author Guido Berhoerster <guido+pui@berhoerster.name>
date Sun, 20 May 2018 11:32:57 +0200
parents
children 2f04ec9e0506
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pui-application.c	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,680 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#define	G_SETTINGS_ENABLE_BACKEND
+#include <gio/gsettingsbackend.h>
+#include <glib/gi18n.h>
+#include <libappindicator/app-indicator.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "pui-common.h"
+#include "pui-application.h"
+#include "pui-types.h"
+#include "pui-backend.h"
+
+#define	SETTINGS_ROOT_PATH \
+    "/org/guido-berhoerster/code/package-update-indicator/"
+#define	SETTINGS_ROOT_GROUP	"General"
+
+struct _PuiApplication {
+	GApplication	parent_instance;
+	GSettings	*settings;
+	GCancellable	*cancellable;
+	PuiBackend	*backend;
+	AppIndicator	*indicator;
+	GtkWidget	*about_dialog;
+	GIcon		*icons[PUI_STATE_LAST];
+	PuiState	state;
+	gchar		*update_command;
+	gchar		*error_message;
+};
+
+G_DEFINE_TYPE(PuiApplication, pui_application, G_TYPE_APPLICATION)
+
+enum {
+    PROP_0,
+    PROP_UPDATE_COMMAND,
+    PROP_LAST
+};
+
+extern gboolean	restart;
+
+static GParamSpec *properties[PROP_LAST] = { NULL };
+
+static const gchar *icon_names[PUI_STATE_LAST] = {
+    [PUI_STATE_INITIAL] = "system-software-update",
+    [PUI_STATE_UP_TO_DATE] = "system-software-update",
+    [PUI_STATE_NORMAL_UPDATES_AVAILABLE] = "software-update-available",
+    [PUI_STATE_IMPORTANT_UPDATES_AVAILABLE] = "software-update-urgent",
+    [PUI_STATE_ERROR] = "dialog-warning"
+};
+
+static const GOptionEntry cmd_options[] = {
+    { "debug", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
+    N_("Enable debugging messages"), NULL },
+    { "quit", 'q', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
+    N_("Quit running instance of Package Update Indicator"), NULL },
+    { "version", 'V', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
+    N_("Print the version number and quit"), NULL },
+    { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, NULL, NULL, NULL },
+    { NULL }
+};
+
+static void	pui_application_show_about_dialog(GSimpleAction *, GVariant *,
+    gpointer);
+static void	pui_application_quit(GSimpleAction *, GVariant *, gpointer);
+static void	pui_application_install_updates(GSimpleAction *, GVariant *,
+    gpointer);
+
+static const GActionEntry pui_application_actions[] = {
+    { "about", pui_application_show_about_dialog },
+    { "quit", pui_application_quit },
+    { "install-updates", pui_application_install_updates }
+};
+
+static gboolean
+program_exists(const gchar *command_line)
+{
+	gboolean	is_program_in_path;
+	gchar		**argv = NULL;
+	gchar		*program_path;
+
+	if (!g_shell_parse_argv(command_line, NULL, &argv, NULL)) {
+		return (FALSE);
+	}
+	program_path = g_find_program_in_path(argv[0]);
+	is_program_in_path = (program_path != NULL) ? TRUE : FALSE;
+	g_free(program_path);
+	g_strfreev(argv);
+
+	return (is_program_in_path);
+}
+
+static void
+pui_application_show_about_dialog(GSimpleAction *simple, GVariant *parameter,
+    gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+	const gchar	*translators;
+
+	if (self->about_dialog == NULL) {
+		translators = _("Translators");
+		if (strcmp(translators, "Translators") == 0) {
+			translators = NULL;
+		}
+
+		self->about_dialog = gtk_about_dialog_new();
+		g_object_set(G_OBJECT(self->about_dialog),
+		    "authors", (gchar *[]){ "Guido Berhoerster", NULL },
+		    "comments", _("Notify about available software updates"),
+		    "copyright", "Copyright \xc2\xa9 2018 Guido Berhoerster",
+		    "license-type", GTK_LICENSE_MIT_X11,
+		    "logo-icon-name", "system-software-update",
+		    "translator-credits", translators,
+		    "version", VERSION,
+		    "website", "https://code.guido-berhoerster.org/projects/"
+		    "package-update-indicator/",
+		    "wrap-license", TRUE,
+		    NULL);
+	}
+
+	gtk_dialog_run(GTK_DIALOG(self->about_dialog));
+	gtk_widget_hide(self->about_dialog);
+}
+
+static void
+pui_application_quit(GSimpleAction *simple, GVariant *parameter,
+    gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+
+	/* quit the GTK main loop if the about dialog is running */
+	if (self->about_dialog != NULL) {
+		gtk_widget_hide(self->about_dialog);
+	}
+
+	g_application_quit(G_APPLICATION(self));
+}
+
+static void
+pui_application_install_updates(GSimpleAction *simple, GVariant *parameter,
+    gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+	GError		*error = NULL;
+
+	if (!g_spawn_command_line_async(self->update_command, &error)) {
+		g_warning("failed to run update command: %s", error->message);
+		g_error_free(error);
+	}
+}
+
+static void
+update_ui(PuiApplication *self)
+{
+	GSimpleAction	*install_updates_action;
+	guint		important_updates = 0;
+	guint		normal_updates = 0;
+	gchar		*title = NULL;
+	gchar		*body = NULL;
+	GApplication	*application = G_APPLICATION(self);
+	GNotification	*notification = NULL;
+
+	install_updates_action =
+	    G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(self),
+	    "install-updates"));
+
+	if ((self->state == PUI_STATE_NORMAL_UPDATES_AVAILABLE) ||
+	    (self->state == PUI_STATE_IMPORTANT_UPDATES_AVAILABLE)) {
+		g_object_get(self->backend,
+		    "important-updates", &important_updates,
+		    "normal-updates", &normal_updates, NULL);
+	}
+
+	/* actions */
+	switch (self->state) {
+	case PUI_STATE_INITIAL:				/* FALLTHGROUGH */
+	case PUI_STATE_UP_TO_DATE:			/* FALLTHGROUGH */
+	case PUI_STATE_ERROR:
+		g_simple_action_set_enabled(install_updates_action, FALSE);
+		break;
+	case PUI_STATE_NORMAL_UPDATES_AVAILABLE:	/* FALLTHROUGH */
+	case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE:
+		g_simple_action_set_enabled(install_updates_action,
+		    program_exists(self->update_command));
+		break;
+	}
+
+	/* title and body for indicator and notification */
+	switch (self->state) {
+	case PUI_STATE_INITIAL:
+		title = g_strdup("");
+		body = g_strdup("");
+		break;
+	case PUI_STATE_UP_TO_DATE:
+		title = g_strdup(_("Up to Date"));
+		body = g_strdup(_("The system is up to date."));
+		break;
+	case PUI_STATE_NORMAL_UPDATES_AVAILABLE:
+		title = g_strdup(g_dngettext(NULL, "Software Update",
+		    "Software Updates", (gulong)normal_updates));
+		if (normal_updates == 1) {
+			body = g_strdup(_("There is a software update "
+			    "avaliable."));
+		} else {
+			body = g_strdup_printf(_("There are %u "
+			    "software updates avaliable."), normal_updates);
+		}
+		break;
+	case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE:
+		title = g_strdup(g_dngettext(NULL, "Important Software Update",
+		    "Important Software Updates", (gulong)important_updates));
+		if ((normal_updates == 0) && (important_updates == 1)) {
+			body = g_strdup(_("There is an important "
+			    "software update available."));
+		} else if ((normal_updates == 0) && (important_updates > 1)) {
+			body = g_strdup_printf(_("There are %u "
+			    "important software updates available."),
+			    important_updates);
+		} else if ((normal_updates > 0) && (important_updates == 1)) {
+			body = g_strdup_printf(_("There are %u "
+			    "software updates available, "
+			    "one of them is important."),
+			    normal_updates + important_updates);
+		} else {
+			body = g_strdup_printf(_("There are %u "
+			    "software updates available, "
+			    "%u of them are important."),
+			    normal_updates + important_updates,
+			    important_updates);
+		}
+		break;
+	case PUI_STATE_ERROR:
+		title = g_strdup(self->error_message);
+		break;
+	}
+
+	/* indicator */
+	switch (self->state) {
+	case PUI_STATE_INITIAL:
+		app_indicator_set_status(self->indicator,
+		    APP_INDICATOR_STATUS_PASSIVE);
+		break;
+	case PUI_STATE_UP_TO_DATE:			/* FALLTHGROUGH */
+	case PUI_STATE_NORMAL_UPDATES_AVAILABLE:	/* FALLTHGROUGH */
+	case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE:	/* FALLTHGROUGH */
+	case PUI_STATE_ERROR:
+		app_indicator_set_status(self->indicator,
+		    APP_INDICATOR_STATUS_ACTIVE);
+		break;
+	}
+	app_indicator_set_icon_full(self->indicator, icon_names[self->state],
+	    title);
+
+	/* notification */
+	switch (self->state) {
+	case PUI_STATE_INITIAL:				/* FALLTHGROUGH */
+	case PUI_STATE_UP_TO_DATE:			/* FALLTHGROUGH */
+	case PUI_STATE_ERROR:
+		/* withdraw exisiting notification */
+		g_application_withdraw_notification(application,
+		    "package-updates");
+		break;
+	case PUI_STATE_NORMAL_UPDATES_AVAILABLE:	/* FALLTHGROUGH */
+	case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE:
+		/* create notification */
+		notification = g_notification_new(title);
+		g_notification_set_body(notification, body);
+		g_notification_set_icon(notification, self->icons[self->state]);
+		g_notification_set_priority(notification,
+		    G_NOTIFICATION_PRIORITY_NORMAL);
+		if (g_action_get_enabled(G_ACTION(install_updates_action))) {
+			g_notification_add_button(notification,
+			    _("Install Updates"),
+			    "app.install-updates");
+		}
+		g_application_send_notification(application, "package-updates",
+		    notification);
+		break;
+	}
+
+	if (notification != NULL) {
+		g_object_unref(notification);
+	}
+
+	g_debug("indicator icon: %s, notification title: \"%s\", "
+	    "notification body: \"%s\"", icon_names[self->state], title, body);
+
+	g_free(body);
+	g_free(title);
+}
+
+static void
+transition_state(PuiApplication *self)
+{
+	PuiState	state = self->state;
+	guint		important_updates;
+	guint		normal_updates;
+	gchar		*old_state;
+	gchar		*new_state;
+
+	switch (self->state) {
+	case PUI_STATE_INITIAL:				/* FALLTHROUGH */
+	case PUI_STATE_UP_TO_DATE:			/* FALLTHROUGH */
+	case PUI_STATE_NORMAL_UPDATES_AVAILABLE:	/* FALLTHROUGH */
+	case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE:
+		if (self->error_message != NULL) {
+			state = PUI_STATE_ERROR;
+			break;
+		}
+
+		g_object_get(self->backend,
+		    "important-updates", &important_updates,
+		    "normal-updates", &normal_updates, NULL);
+		if (important_updates > 0) {
+			state = PUI_STATE_IMPORTANT_UPDATES_AVAILABLE;
+		} else if (normal_updates > 0) {
+			state = PUI_STATE_NORMAL_UPDATES_AVAILABLE;
+		} else {
+			state = PUI_STATE_UP_TO_DATE;
+		}
+		break;
+	case PUI_STATE_ERROR:
+		break;
+	}
+
+	if (state != self->state) {
+		old_state = pui_types_enum_to_string(PUI_TYPE_STATE,
+		    self->state);
+		new_state = pui_types_enum_to_string(PUI_TYPE_STATE, state);
+		g_debug("state %s -> %s", old_state, new_state);
+
+		self->state = state;
+		update_ui(self);
+
+		g_free(new_state);
+		g_free(old_state);
+	}
+}
+
+static void
+on_backend_restart_required(PuiBackend *backend, gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+
+	restart = TRUE;
+	g_action_group_activate_action(G_ACTION_GROUP(G_APPLICATION(self)),
+	    "quit", NULL);
+}
+
+static void
+on_backend_state_changed(PuiBackend *backend, gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+
+	transition_state(self);
+}
+
+static void
+on_pui_backend_finished(GObject *source_object, GAsyncResult *result,
+    gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+	GError		*error = NULL;
+
+	self->backend = pui_backend_new_finish(result, &error);
+	if (self->backend == NULL) {
+		g_warning("failed to instantiate backend: %s", error->message);
+		g_free(self->error_message);
+		g_error_free(error);
+		self->error_message = g_strdup(_("Update notifications "
+		    "are not supported."));
+		transition_state(self);
+		return;
+	}
+
+	g_settings_bind(self->settings, "refresh-interval", self->backend,
+	    "refresh-interval", G_SETTINGS_BIND_GET);
+
+	transition_state(self);
+
+	g_signal_connect(self->backend, "restart-required",
+	    G_CALLBACK(on_backend_restart_required), self);
+	g_signal_connect(self->backend, "state-changed",
+	    G_CALLBACK(on_backend_state_changed), self);
+}
+
+static void
+pui_application_startup(GApplication *application)
+{
+	PuiApplication	*self = PUI_APPLICATION(application);
+	gsize		i;
+	gchar		*settings_filename;
+	GSettingsBackend *settings_backend;
+	GtkWidget	*menu;
+	GtkWidget	*menu_item;
+
+	G_APPLICATION_CLASS(pui_application_parent_class)->startup(application);
+
+	/* create actions */
+	g_action_map_add_action_entries(G_ACTION_MAP(self),
+	    pui_application_actions, G_N_ELEMENTS(pui_application_actions),
+	    self);
+
+	/* load icons */
+	for (i = 0; i < G_N_ELEMENTS(self->icons); i++) {
+		self->icons[i] = g_themed_icon_new(icon_names[i]);
+	}
+
+	/* create settings */
+	settings_filename = g_build_filename(g_get_user_config_dir(), PACKAGE,
+	    PACKAGE ".conf", NULL);
+	settings_backend = g_keyfile_settings_backend_new(settings_filename,
+	    SETTINGS_ROOT_PATH, SETTINGS_ROOT_GROUP);
+	self->settings = g_settings_new_with_backend(APPLICATION_ID,
+	    settings_backend);
+	g_settings_bind(self->settings, "update-command", self,
+	    "update-command", G_SETTINGS_BIND_GET);
+
+	/* start instantiating backend */
+	pui_backend_new_async(self->cancellable, on_pui_backend_finished, self);
+
+	/* create indicator */
+	self->indicator = app_indicator_new(APPLICATION_ID,
+	    "system-software-update",
+	    APP_INDICATOR_CATEGORY_APPLICATION_STATUS);
+
+	/* build menu */
+	menu = gtk_menu_new();
+	gtk_widget_insert_action_group(GTK_WIDGET(menu), "app",
+	    G_ACTION_GROUP(self));
+
+	menu_item = gtk_menu_item_new_with_label(_("Install "
+	    "Updates\342\200\246"));
+	gtk_actionable_set_action_name(GTK_ACTIONABLE(menu_item),
+	    "app.install-updates");
+	gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+
+	menu_item = gtk_menu_item_new_with_label(_("About\342\200\246"));
+	gtk_actionable_set_action_name(GTK_ACTIONABLE(menu_item),
+	    "app.about");
+	gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+
+	gtk_widget_show_all(menu);
+	app_indicator_set_menu(self->indicator, GTK_MENU(menu));
+
+	update_ui(self);
+
+	/* keep GApplication running */
+	g_application_hold(application);
+
+	g_object_unref(settings_backend);
+	g_free(settings_filename);
+}
+
+static void
+pui_application_shutdown(GApplication *application)
+{
+	GApplicationClass *application_class =
+	    G_APPLICATION_CLASS(pui_application_parent_class);
+
+	application_class->shutdown(application);
+}
+
+static gint
+pui_application_handle_local_options(GApplication *application,
+    GVariantDict *options)
+{
+	gchar		*messages_debug;
+	gchar		**args = NULL;
+	GError		*error = NULL;
+
+	/* filename arguments are not allowed */
+	if (g_variant_dict_lookup(options, G_OPTION_REMAINING, "^a&ay",
+	    &args)) {
+		g_printerr("invalid argument: \"%s\"\n", args[0]);
+		g_free(args);
+		return (1);
+	}
+
+	if (g_variant_dict_contains(options, "version")) {
+		g_print("%s %s\n", PACKAGE, VERSION);
+
+		/* quit */
+		return (0);
+	}
+
+	if (g_variant_dict_contains(options, "debug")) {
+		/* enable debug logging */
+		messages_debug = g_strjoin(":", G_LOG_DOMAIN,
+		    g_getenv("G_MESSAGES_DEBUG"), NULL);
+		g_setenv("G_MESSAGES_DEBUG", messages_debug, TRUE);
+		g_free(messages_debug);
+	}
+
+	/*
+	 * register with the session bus so that it is possible to discern
+	 * between remote and primary instance and that remote actions can be
+	 * invoked, this causes the startup signal to be emitted which, in case
+	 * of the primary instance, starts to instantiate the
+	 * backend with the given values
+	 */
+	if (!g_application_register(application, NULL, &error)) {
+		g_critical("g_application_register: %s", error->message);
+		g_error_free(error);
+		return (1);
+	}
+
+	if (g_variant_dict_contains(options, "quit")) {
+		/* only valid if a remote instance is running */
+		if (!g_application_get_is_remote(application)) {
+			g_printerr("%s is not running\n", g_get_prgname());
+			return (1);
+		}
+
+		/* signal remote instance to quit */
+		g_action_group_activate_action(G_ACTION_GROUP(application),
+		    "quit", NULL);
+
+		/* quit local instance */
+		return (0);
+	}
+
+	/* proceed with default command line processing */
+	return (-1);
+}
+
+static void
+pui_application_activate(GApplication *application) {
+	GApplicationClass *application_class =
+	    G_APPLICATION_CLASS(pui_application_parent_class);
+
+	/* do nothing, implementation required by GApplication */
+
+	application_class->activate(application);
+}
+
+static void
+pui_application_set_property(GObject *object, guint property_id,
+    const GValue *value, GParamSpec *pspec)
+{
+	PuiApplication	*self = PUI_APPLICATION(object);
+
+	switch (property_id) {
+	case PROP_UPDATE_COMMAND:
+		g_free(self->update_command);
+		self->update_command = g_value_dup_string(value);
+		g_debug("property \"update-command\" set to \"%s\"",
+		    self->update_command);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+		break;
+	}
+}
+
+static void
+pui_application_get_property(GObject *object, guint property_id, GValue *value,
+    GParamSpec *pspec)
+{
+	PuiApplication	*self = PUI_APPLICATION(object);
+
+	switch (property_id) {
+	case PROP_UPDATE_COMMAND:
+		g_value_set_string(value, self->update_command);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+		break;
+	}
+}
+
+static void
+pui_application_dispose(GObject *object)
+{
+	PuiApplication	*self = PUI_APPLICATION(object);
+	gsize		i;
+
+	if (self->settings != NULL) {
+		g_signal_handlers_disconnect_by_data(self->settings, self);
+		g_clear_object(&self->settings);
+	}
+
+	if (self->cancellable != NULL) {
+		g_cancellable_cancel(self->cancellable);
+		g_clear_object(&self->cancellable);
+	}
+
+	if (self->backend != NULL) {
+		g_clear_object(&self->backend);
+	}
+
+	if (self->indicator != NULL) {
+		g_clear_object(&self->indicator);
+	}
+
+	if (self->about_dialog != NULL) {
+		g_clear_pointer(&self->about_dialog,
+		    (GDestroyNotify)(gtk_widget_destroy));
+	}
+
+	for (i = 0; i < G_N_ELEMENTS(self->icons); i++) {
+		if (self->icons[i] != NULL) {
+			g_clear_object(&self->icons[i]);
+		}
+	}
+
+	G_OBJECT_CLASS(pui_application_parent_class)->dispose(object);
+}
+
+static void
+pui_application_finalize(GObject *object)
+{
+	PuiApplication	*self = PUI_APPLICATION(object);
+
+	g_free(self->update_command);
+	g_free(self->error_message);
+
+	G_OBJECT_CLASS(pui_application_parent_class)->finalize(object);
+}
+
+static void
+pui_application_class_init(PuiApplicationClass *klass)
+{
+	GObjectClass	*object_class = G_OBJECT_CLASS(klass);
+	GApplicationClass *application_class = G_APPLICATION_CLASS(klass);
+
+	object_class->set_property = pui_application_set_property;
+	object_class->get_property = pui_application_get_property;
+	object_class->dispose = pui_application_dispose;
+	object_class->finalize = pui_application_finalize;
+
+	properties[PROP_UPDATE_COMMAND] = g_param_spec_string("update-command",
+	    "Update command", "Command for installing updates", NULL,
+	    G_PARAM_READWRITE);
+
+	g_object_class_install_properties(object_class, PROP_LAST, properties);
+
+	application_class->startup = pui_application_startup;
+	application_class->shutdown = pui_application_shutdown;
+	application_class->handle_local_options =
+	    pui_application_handle_local_options;
+	application_class->activate = pui_application_activate;
+}
+
+static void
+pui_application_init(PuiApplication *self)
+{
+	g_application_add_main_option_entries(G_APPLICATION(self),
+	    cmd_options);
+
+	self->cancellable = g_cancellable_new();
+}
+
+PuiApplication *
+pui_application_new(void)
+{
+	return (g_object_new(PUI_TYPE_APPLICATION, "application-id",
+	    APPLICATION_ID, NULL));
+}