diff options
Diffstat (limited to 'plugins')
188 files changed, 42056 insertions, 0 deletions
diff --git a/plugins/a11y-settings/gsd-a11y-settings-manager.c b/plugins/a11y-settings/gsd-a11y-settings-manager.c new file mode 100644 index 0000000..77bb117 --- /dev/null +++ b/plugins/a11y-settings/gsd-a11y-settings-manager.c @@ -0,0 +1,162 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <sys/types.h> +#include <sys/wait.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> + +#include <locale.h> + +#include <glib.h> +#include <gio/gio.h> + +#include "gnome-settings-profile.h" +#include "gsd-a11y-settings-manager.h" + +struct _GsdA11ySettingsManager +{ + GObject parent; + + GSettings *interface_settings; + GSettings *a11y_apps_settings; +}; + +enum { + PROP_0, +}; + +static void gsd_a11y_settings_manager_class_init (GsdA11ySettingsManagerClass *klass); +static void gsd_a11y_settings_manager_init (GsdA11ySettingsManager *a11y_settings_manager); +static void gsd_a11y_settings_manager_finalize (GObject *object); + +G_DEFINE_TYPE (GsdA11ySettingsManager, gsd_a11y_settings_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +static void +apps_settings_changed (GSettings *settings, + const char *key, + GsdA11ySettingsManager *manager) +{ + gboolean screen_reader, keyboard; + + if (g_str_equal (key, "screen-reader-enabled") == FALSE && + g_str_equal (key, "screen-keyboard-enabled") == FALSE) + return; + + g_debug ("screen reader or OSK enablement changed"); + + screen_reader = g_settings_get_boolean (manager->a11y_apps_settings, "screen-reader-enabled"); + keyboard = g_settings_get_boolean (manager->a11y_apps_settings, "screen-keyboard-enabled"); + + if (screen_reader || keyboard) { + g_debug ("Enabling toolkit-accessibility, screen reader or OSK enabled"); + g_settings_set_boolean (manager->interface_settings, "toolkit-accessibility", TRUE); + } else if (screen_reader == FALSE && keyboard == FALSE) { + g_debug ("Disabling toolkit-accessibility, screen reader and OSK disabled"); + g_settings_set_boolean (manager->interface_settings, "toolkit-accessibility", FALSE); + } +} + +gboolean +gsd_a11y_settings_manager_start (GsdA11ySettingsManager *manager, + GError **error) +{ + g_debug ("Starting a11y_settings manager"); + gnome_settings_profile_start (NULL); + + manager->interface_settings = g_settings_new ("org.gnome.desktop.interface"); + manager->a11y_apps_settings = g_settings_new ("org.gnome.desktop.a11y.applications"); + + g_signal_connect (G_OBJECT (manager->a11y_apps_settings), "changed", + G_CALLBACK (apps_settings_changed), manager); + + /* If any of the screen reader or on-screen keyboard are enabled, + * make sure a11y is enabled for the toolkits. + * We don't do the same thing for the reverse so it's possible to + * enable AT-SPI for the toolkits without using an a11y app */ + if (g_settings_get_boolean (manager->a11y_apps_settings, "screen-keyboard-enabled") || + g_settings_get_boolean (manager->a11y_apps_settings, "screen-reader-enabled")) + g_settings_set_boolean (manager->interface_settings, "toolkit-accessibility", TRUE); + + gnome_settings_profile_end (NULL); + return TRUE; +} + +void +gsd_a11y_settings_manager_stop (GsdA11ySettingsManager *manager) +{ + if (manager->interface_settings) { + g_object_unref (manager->interface_settings); + manager->interface_settings = NULL; + } + if (manager->a11y_apps_settings) { + g_object_unref (manager->a11y_apps_settings); + manager->a11y_apps_settings = NULL; + } + g_debug ("Stopping a11y_settings manager"); +} + +static void +gsd_a11y_settings_manager_class_init (GsdA11ySettingsManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_a11y_settings_manager_finalize; +} + +static void +gsd_a11y_settings_manager_init (GsdA11ySettingsManager *manager) +{ +} + +static void +gsd_a11y_settings_manager_finalize (GObject *object) +{ + GsdA11ySettingsManager *a11y_settings_manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_A11Y_SETTINGS_MANAGER (object)); + + a11y_settings_manager = GSD_A11Y_SETTINGS_MANAGER (object); + + gsd_a11y_settings_manager_stop (a11y_settings_manager); + + G_OBJECT_CLASS (gsd_a11y_settings_manager_parent_class)->finalize (object); +} + +GsdA11ySettingsManager * +gsd_a11y_settings_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_A11Y_SETTINGS_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_A11Y_SETTINGS_MANAGER (manager_object); +} diff --git a/plugins/a11y-settings/gsd-a11y-settings-manager.h b/plugins/a11y-settings/gsd-a11y-settings-manager.h new file mode 100644 index 0000000..56a2bbb --- /dev/null +++ b/plugins/a11y-settings/gsd-a11y-settings-manager.h @@ -0,0 +1,37 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_A11Y_SETTINGS_MANAGER_H +#define __GSD_A11Y_SETTINGS_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_A11Y_SETTINGS_MANAGER gsd_a11y_settings_manager_get_type () +G_DECLARE_FINAL_TYPE (GsdA11ySettingsManager, gsd_a11y_settings_manager, GSD, A11Y_SETTINGS_MANAGER, GObject) + +GsdA11ySettingsManager *gsd_a11y_settings_manager_new (void); +gboolean gsd_a11y_settings_manager_start (GsdA11ySettingsManager *manager, + GError **error); +void gsd_a11y_settings_manager_stop (GsdA11ySettingsManager *manager); + +G_END_DECLS + +#endif /* __GSD_A11Y_SETTINGS_MANAGER_H */ diff --git a/plugins/a11y-settings/main.c b/plugins/a11y-settings/main.c new file mode 100644 index 0000000..aff6cd5 --- /dev/null +++ b/plugins/a11y-settings/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_a11y_settings_manager_new +#define START gsd_a11y_settings_manager_start +#define STOP gsd_a11y_settings_manager_stop +#define MANAGER GsdA11ySettingsManager +#include "gsd-a11y-settings-manager.h" + +#include "daemon-skeleton.h" diff --git a/plugins/a11y-settings/meson.build b/plugins/a11y-settings/meson.build new file mode 100644 index 0000000..b4a0fb6 --- /dev/null +++ b/plugins/a11y-settings/meson.build @@ -0,0 +1,20 @@ +sources = files( + 'gsd-a11y-settings-manager.c', + 'main.c' +) + +deps = plugins_deps + [ + gio_dep, + gsettings_desktop_dep +] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) diff --git a/plugins/color/gcm-edid.c b/plugins/color/gcm-edid.c new file mode 100644 index 0000000..a00d2ce --- /dev/null +++ b/plugins/color/gcm-edid.c @@ -0,0 +1,450 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Soren Sandmann <sandmann@redhat.com> + * Copyright (C) 2009-2011 Richard Hughes <richard@hughsie.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <glib-object.h> +#include <math.h> +#include <string.h> +#include <gio/gio.h> +#include <stdlib.h> +#include <libgnome-desktop/gnome-pnp-ids.h> + +#include "gcm-edid.h" + +static void gcm_edid_finalize (GObject *object); + +struct _GcmEdid +{ + GObject parent; + + gchar *monitor_name; + gchar *vendor_name; + gchar *serial_number; + gchar *eisa_id; + gchar *checksum; + gchar *pnp_id; + guint width; + guint height; + gfloat gamma; + CdColorYxy *red; + CdColorYxy *green; + CdColorYxy *blue; + CdColorYxy *white; + GnomePnpIds *pnp_ids; +}; + +G_DEFINE_TYPE (GcmEdid, gcm_edid, G_TYPE_OBJECT) + +#define GCM_EDID_OFFSET_PNPID 0x08 +#define GCM_EDID_OFFSET_SERIAL 0x0c +#define GCM_EDID_OFFSET_SIZE 0x15 +#define GCM_EDID_OFFSET_GAMMA 0x17 +#define GCM_EDID_OFFSET_DATA_BLOCKS 0x36 +#define GCM_EDID_OFFSET_LAST_BLOCK 0x6c +#define GCM_EDID_OFFSET_EXTENSION_BLOCK_COUNT 0x7e + +#define GCM_DESCRIPTOR_DISPLAY_PRODUCT_NAME 0xfc +#define GCM_DESCRIPTOR_DISPLAY_PRODUCT_SERIAL_NUMBER 0xff +#define GCM_DESCRIPTOR_COLOR_MANAGEMENT_DATA 0xf9 +#define GCM_DESCRIPTOR_ALPHANUMERIC_DATA_STRING 0xfe +#define GCM_DESCRIPTOR_COLOR_POINT 0xfb + +GQuark +gcm_edid_error_quark (void) +{ + static GQuark quark = 0; + if (!quark) + quark = g_quark_from_static_string ("gcm_edid_error"); + return quark; +} + +const gchar * +gcm_edid_get_monitor_name (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), NULL); + return edid->monitor_name; +} + +const gchar * +gcm_edid_get_vendor_name (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), NULL); + + if (edid->vendor_name == NULL) + edid->vendor_name = gnome_pnp_ids_get_pnp_id (edid->pnp_ids, edid->pnp_id); + return edid->vendor_name; +} + +const gchar * +gcm_edid_get_serial_number (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), NULL); + return edid->serial_number; +} + +const gchar * +gcm_edid_get_eisa_id (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), NULL); + return edid->eisa_id; +} + +const gchar * +gcm_edid_get_checksum (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), NULL); + return edid->checksum; +} + +const gchar * +gcm_edid_get_pnp_id (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), NULL); + return edid->pnp_id; +} + +guint +gcm_edid_get_width (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), 0); + return edid->width; +} + +guint +gcm_edid_get_height (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), 0); + return edid->height; +} + +gfloat +gcm_edid_get_gamma (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), 0.0f); + return edid->gamma; +} + +const CdColorYxy * +gcm_edid_get_red (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), NULL); + return edid->red; +} + +const CdColorYxy * +gcm_edid_get_green (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), NULL); + return edid->green; +} + +const CdColorYxy * +gcm_edid_get_blue (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), NULL); + return edid->blue; +} + +const CdColorYxy * +gcm_edid_get_white (GcmEdid *edid) +{ + g_return_val_if_fail (GCM_IS_EDID (edid), NULL); + return edid->white; +} + +void +gcm_edid_reset (GcmEdid *edid) +{ + g_return_if_fail (GCM_IS_EDID (edid)); + + /* free old data */ + g_free (edid->monitor_name); + g_free (edid->vendor_name); + g_free (edid->serial_number); + g_free (edid->eisa_id); + g_free (edid->checksum); + + /* do not deallocate, just blank */ + edid->pnp_id[0] = '\0'; + + /* set to default values */ + edid->monitor_name = NULL; + edid->vendor_name = NULL; + edid->serial_number = NULL; + edid->eisa_id = NULL; + edid->checksum = NULL; + edid->width = 0; + edid->height = 0; + edid->gamma = 0.0f; +} + +static gint +gcm_edid_get_bit (gint in, gint bit) +{ + return (in & (1 << bit)) >> bit; +} + +/** + * gcm_edid_get_bits: + **/ +static gint +gcm_edid_get_bits (gint in, gint begin, gint end) +{ + gint mask = (1 << (end - begin + 1)) - 1; + + return (in >> begin) & mask; +} + +/** + * gcm_edid_decode_fraction: + **/ +static gdouble +gcm_edid_decode_fraction (gint high, gint low) +{ + gdouble result = 0.0; + gint i; + + high = (high << 2) | low; + for (i = 0; i < 10; ++i) + result += gcm_edid_get_bit (high, i) * pow (2, i - 10); + return result; +} + +static gchar * +gcm_edid_parse_string (const guint8 *data) +{ + gchar *text; + guint i; + guint replaced = 0; + + /* this is always 13 bytes, but we can't guarantee it's null + * terminated or not junk. */ + text = g_strndup ((const gchar *) data, 13); + + /* remove insane newline chars */ + g_strdelimit (text, "\n\r", '\0'); + + /* remove spaces */ + g_strchomp (text); + + /* nothing left? */ + if (text[0] == '\0') { + g_free (text); + text = NULL; + goto out; + } + + /* ensure string is printable */ + for (i = 0; text[i] != '\0'; i++) { + if (!g_ascii_isprint (text[i])) { + text[i] = '-'; + replaced++; + } + } + + /* if the string is junk, ignore the string */ + if (replaced > 4) { + g_free (text); + text = NULL; + goto out; + } +out: + return text; +} + +gboolean +gcm_edid_parse (GcmEdid *edid, const guint8 *data, gsize length, GError **error) +{ + gboolean ret = TRUE; + guint i; + guint32 serial; + gchar *tmp; + + /* check header */ + if (length < 128) { + g_set_error_literal (error, + GCM_EDID_ERROR, + GCM_EDID_ERROR_FAILED_TO_PARSE, + "EDID length is too small"); + ret = FALSE; + goto out; + } + if (data[0] != 0x00 || data[1] != 0xff) { + g_set_error_literal (error, + GCM_EDID_ERROR, + GCM_EDID_ERROR_FAILED_TO_PARSE, + "Failed to parse EDID header"); + ret = FALSE; + goto out; + } + + /* free old data */ + gcm_edid_reset (edid); + + /* decode the PNP ID from three 5 bit words packed into 2 bytes + * /--08--\/--09--\ + * 7654321076543210 + * |\---/\---/\---/ + * R C1 C2 C3 */ + edid->pnp_id[0] = 'A' + ((data[GCM_EDID_OFFSET_PNPID+0] & 0x7c) / 4) - 1; + edid->pnp_id[1] = 'A' + ((data[GCM_EDID_OFFSET_PNPID+0] & 0x3) * 8) + ((data[GCM_EDID_OFFSET_PNPID+1] & 0xe0) / 32) - 1; + edid->pnp_id[2] = 'A' + (data[GCM_EDID_OFFSET_PNPID+1] & 0x1f) - 1; + + /* maybe there isn't a ASCII serial number descriptor, so use this instead */ + serial = (guint32) data[GCM_EDID_OFFSET_SERIAL+0]; + serial += (guint32) data[GCM_EDID_OFFSET_SERIAL+1] * 0x100; + serial += (guint32) data[GCM_EDID_OFFSET_SERIAL+2] * 0x10000; + serial += (guint32) data[GCM_EDID_OFFSET_SERIAL+3] * 0x1000000; + if (serial > 0) + edid->serial_number = g_strdup_printf ("%" G_GUINT32_FORMAT, serial); + + /* get the size */ + edid->width = data[GCM_EDID_OFFSET_SIZE+0]; + edid->height = data[GCM_EDID_OFFSET_SIZE+1]; + + /* we don't care about aspect */ + if (edid->width == 0 || edid->height == 0) { + edid->width = 0; + edid->height = 0; + } + + /* get gamma */ + if (data[GCM_EDID_OFFSET_GAMMA] == 0xff) { + edid->gamma = 1.0f; + } else { + edid->gamma = ((gfloat) data[GCM_EDID_OFFSET_GAMMA] / 100) + 1; + } + + /* get color red */ + edid->red->x = gcm_edid_decode_fraction (data[0x1b], gcm_edid_get_bits (data[0x19], 6, 7)); + edid->red->y = gcm_edid_decode_fraction (data[0x1c], gcm_edid_get_bits (data[0x19], 4, 5)); + + /* get color green */ + edid->green->x = gcm_edid_decode_fraction (data[0x1d], gcm_edid_get_bits (data[0x19], 2, 3)); + edid->green->y = gcm_edid_decode_fraction (data[0x1e], gcm_edid_get_bits (data[0x19], 0, 1)); + + /* get color blue */ + edid->blue->x = gcm_edid_decode_fraction (data[0x1f], gcm_edid_get_bits (data[0x1a], 6, 7)); + edid->blue->y = gcm_edid_decode_fraction (data[0x20], gcm_edid_get_bits (data[0x1a], 4, 5)); + + /* get color white */ + edid->white->x = gcm_edid_decode_fraction (data[0x21], gcm_edid_get_bits (data[0x1a], 2, 3)); + edid->white->y = gcm_edid_decode_fraction (data[0x22], gcm_edid_get_bits (data[0x1a], 0, 1)); + + /* parse EDID data */ + for (i = GCM_EDID_OFFSET_DATA_BLOCKS; + i <= GCM_EDID_OFFSET_LAST_BLOCK; + i += 18) { + /* ignore pixel clock data */ + if (data[i] != 0) + continue; + if (data[i+2] != 0) + continue; + + /* any useful blocks? */ + if (data[i+3] == GCM_DESCRIPTOR_DISPLAY_PRODUCT_NAME) { + tmp = gcm_edid_parse_string (&data[i+5]); + if (tmp != NULL) { + g_free (edid->monitor_name); + edid->monitor_name = tmp; + } + } else if (data[i+3] == GCM_DESCRIPTOR_DISPLAY_PRODUCT_SERIAL_NUMBER) { + tmp = gcm_edid_parse_string (&data[i+5]); + if (tmp != NULL) { + g_free (edid->serial_number); + edid->serial_number = tmp; + } + } else if (data[i+3] == GCM_DESCRIPTOR_COLOR_MANAGEMENT_DATA) { + g_warning ("failing to parse color management data"); + } else if (data[i+3] == GCM_DESCRIPTOR_ALPHANUMERIC_DATA_STRING) { + tmp = gcm_edid_parse_string (&data[i+5]); + if (tmp != NULL) { + g_free (edid->eisa_id); + edid->eisa_id = tmp; + } + } else if (data[i+3] == GCM_DESCRIPTOR_COLOR_POINT) { + if (data[i+3+9] != 0xff) { + /* extended EDID block(1) which contains + * a better gamma value */ + edid->gamma = ((gfloat) data[i+3+9] / 100) + 1; + } + if (data[i+3+14] != 0xff) { + /* extended EDID block(2) which contains + * a better gamma value */ + edid->gamma = ((gfloat) data[i+3+9] / 100) + 1; + } + } + } + + /* calculate checksum */ + edid->checksum = g_compute_checksum_for_data (G_CHECKSUM_MD5, data, length); +out: + return ret; +} + +static void +gcm_edid_class_init (GcmEdidClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gcm_edid_finalize; +} + +static void +gcm_edid_init (GcmEdid *edid) +{ + edid->pnp_ids = gnome_pnp_ids_new (); + edid->pnp_id = g_new0 (gchar, 4); + edid->red = cd_color_yxy_new (); + edid->green = cd_color_yxy_new (); + edid->blue = cd_color_yxy_new (); + edid->white = cd_color_yxy_new (); +} + +static void +gcm_edid_finalize (GObject *object) +{ + GcmEdid *edid = GCM_EDID (object); + + g_free (edid->monitor_name); + g_free (edid->vendor_name); + g_free (edid->serial_number); + g_free (edid->eisa_id); + g_free (edid->checksum); + g_free (edid->pnp_id); + cd_color_yxy_free (edid->white); + cd_color_yxy_free (edid->red); + cd_color_yxy_free (edid->green); + cd_color_yxy_free (edid->blue); + g_object_unref (edid->pnp_ids); + + G_OBJECT_CLASS (gcm_edid_parent_class)->finalize (object); +} + +GcmEdid * +gcm_edid_new (void) +{ + GcmEdid *edid; + edid = g_object_new (GCM_TYPE_EDID, NULL); + return GCM_EDID (edid); +} + diff --git a/plugins/color/gcm-edid.h b/plugins/color/gcm-edid.h new file mode 100644 index 0000000..d50a32e --- /dev/null +++ b/plugins/color/gcm-edid.h @@ -0,0 +1,63 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2009-2010 Richard Hughes <richard@hughsie.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __GCM_EDID_H +#define __GCM_EDID_H + +#include <glib-object.h> +#include <colord.h> + +G_BEGIN_DECLS + +#define GCM_TYPE_EDID (gcm_edid_get_type ()) +G_DECLARE_FINAL_TYPE (GcmEdid, gcm_edid, GCM, EDID, GObject) + +#define GCM_EDID_ERROR (gcm_edid_error_quark ()) +enum +{ + GCM_EDID_ERROR_FAILED_TO_PARSE +}; + +GQuark gcm_edid_error_quark (void); +GcmEdid *gcm_edid_new (void); +void gcm_edid_reset (GcmEdid *edid); +gboolean gcm_edid_parse (GcmEdid *edid, + const guint8 *data, + gsize length, + GError **error); +const gchar *gcm_edid_get_monitor_name (GcmEdid *edid); +const gchar *gcm_edid_get_vendor_name (GcmEdid *edid); +const gchar *gcm_edid_get_serial_number (GcmEdid *edid); +const gchar *gcm_edid_get_eisa_id (GcmEdid *edid); +const gchar *gcm_edid_get_checksum (GcmEdid *edid); +const gchar *gcm_edid_get_pnp_id (GcmEdid *edid); +guint gcm_edid_get_width (GcmEdid *edid); +guint gcm_edid_get_height (GcmEdid *edid); +gfloat gcm_edid_get_gamma (GcmEdid *edid); +const CdColorYxy *gcm_edid_get_red (GcmEdid *edid); +const CdColorYxy *gcm_edid_get_green (GcmEdid *edid); +const CdColorYxy *gcm_edid_get_blue (GcmEdid *edid); +const CdColorYxy *gcm_edid_get_white (GcmEdid *edid); + +G_END_DECLS + +#endif /* __GCM_EDID_H */ + diff --git a/plugins/color/gcm-self-test.c b/plugins/color/gcm-self-test.c new file mode 100644 index 0000000..7ff01f6 --- /dev/null +++ b/plugins/color/gcm-self-test.c @@ -0,0 +1,430 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2007-2011 Richard Hughes <richard@hughsie.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> +#include <stdlib.h> + +#include "gcm-edid.h" +#include "gsd-color-state.h" +#include "gsd-night-light.h" +#include "gsd-night-light-common.h" + +GMainLoop *mainloop; + +static void +on_notify (GsdNightLight *nlight, + GParamSpec *pspec, + gpointer user_data) +{ + guint *cnt = (guint *) user_data; + (*cnt)++; +} + +static gboolean +quit_mainloop (gpointer user_data) +{ + g_main_loop_quit (mainloop); + + return FALSE; +} + +static void +gcm_test_night_light (void) +{ + gboolean ret; + guint active_cnt = 0; + guint disabled_until_tmw_cnt = 0; + guint sunrise_cnt = 0; + guint sunset_cnt = 0; + guint temperature_cnt = 0; + g_autoptr(GDateTime) datetime_override = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsdNightLight) nlight = NULL; + g_autoptr(GSettings) settings = NULL; + + nlight = gsd_night_light_new (); + g_assert (GSD_IS_NIGHT_LIGHT (nlight)); + g_signal_connect (nlight, "notify::active", + G_CALLBACK (on_notify), &active_cnt); + g_signal_connect (nlight, "notify::sunset", + G_CALLBACK (on_notify), &sunset_cnt); + g_signal_connect (nlight, "notify::sunrise", + G_CALLBACK (on_notify), &sunrise_cnt); + g_signal_connect (nlight, "notify::temperature", + G_CALLBACK (on_notify), &temperature_cnt); + g_signal_connect (nlight, "notify::disabled-until-tmw", + G_CALLBACK (on_notify), &disabled_until_tmw_cnt); + + /* hardcode a specific date and time */ + datetime_override = g_date_time_new_utc (2017, 2, 8, 20, 0, 0); + gsd_night_light_set_date_time_now (nlight, datetime_override); + + /* do not start geoclue */ + gsd_night_light_set_geoclue_enabled (nlight, FALSE); + + /* do not smooth the transition */ + gsd_night_light_set_smooth_enabled (nlight, FALSE); + + /* switch off */ + settings = g_settings_new ("org.gnome.settings-daemon.plugins.color"); + g_settings_set_boolean (settings, "night-light-enabled", FALSE); + g_settings_set_uint (settings, "night-light-temperature", 4000); + + /* check default values */ + g_assert (!gsd_night_light_get_active (nlight)); + g_assert_cmpint ((gint) gsd_night_light_get_sunrise (nlight), ==, -1); + g_assert_cmpint ((gint) gsd_night_light_get_sunset (nlight), ==, -1); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT); + g_assert (!gsd_night_light_get_disabled_until_tmw (nlight)); + + /* start module, disabled */ + ret = gsd_night_light_start (nlight, &error); + g_assert_no_error (error); + g_assert (ret); + g_assert (!gsd_night_light_get_active (nlight)); + g_assert_cmpint (active_cnt, ==, 0); + g_assert_cmpint (sunset_cnt, ==, 0); + g_assert_cmpint (sunrise_cnt, ==, 0); + g_assert_cmpint (temperature_cnt, ==, 0); + g_assert_cmpint (disabled_until_tmw_cnt, ==, 0); + + /* enable automatic mode */ + g_settings_set_value (settings, "night-light-last-coordinates", + g_variant_new ("(dd)", 51.5, -0.1278)); + g_settings_set_boolean (settings, "night-light-schedule-automatic", TRUE); + g_settings_set_boolean (settings, "night-light-enabled", TRUE); + g_assert (gsd_night_light_get_active (nlight)); + g_assert_cmpint (active_cnt, ==, 1); + g_assert_cmpint (sunset_cnt, ==, 1); + g_assert_cmpint (sunrise_cnt, ==, 1); + g_assert_cmpint (temperature_cnt, ==, 1); + g_assert_cmpint (disabled_until_tmw_cnt, ==, 0); + g_assert_cmpint ((gint) gsd_night_light_get_sunrise (nlight), ==, 7); + g_assert_cmpint ((gint) gsd_night_light_get_sunset (nlight), ==, 17); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, 4000); + g_assert (!gsd_night_light_get_disabled_until_tmw (nlight)); + + /* disable for one day */ + gsd_night_light_set_disabled_until_tmw (nlight, TRUE); + gsd_night_light_set_disabled_until_tmw (nlight, TRUE); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT); + g_assert (gsd_night_light_get_active (nlight)); + g_assert (gsd_night_light_get_disabled_until_tmw (nlight)); + g_assert_cmpint (temperature_cnt, ==, 2); + g_assert_cmpint (disabled_until_tmw_cnt, ==, 1); + + /* change our mind */ + gsd_night_light_set_disabled_until_tmw (nlight, FALSE); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, 4000); + g_assert (gsd_night_light_get_active (nlight)); + g_assert (!gsd_night_light_get_disabled_until_tmw (nlight)); + g_assert_cmpint (active_cnt, ==, 1); + g_assert_cmpint (temperature_cnt, ==, 3); + g_assert_cmpint (disabled_until_tmw_cnt, ==, 2); + + /* enabled manual mode (night shift) */ + g_settings_set_double (settings, "night-light-schedule-from", 4.0); + g_settings_set_double (settings, "night-light-schedule-to", 16.f); + g_settings_set_boolean (settings, "night-light-schedule-automatic", FALSE); + g_assert_cmpint (active_cnt, ==, 2); + g_assert_cmpint (sunset_cnt, ==, 1); + g_assert_cmpint (sunrise_cnt, ==, 1); + g_assert_cmpint (temperature_cnt, ==, 4); + g_assert_cmpint (disabled_until_tmw_cnt, ==, 2); + g_assert (!gsd_night_light_get_active (nlight)); + g_assert_cmpint ((gint) gsd_night_light_get_sunrise (nlight), ==, 7); + g_assert_cmpint ((gint) gsd_night_light_get_sunset (nlight), ==, 17); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT); + g_assert (!gsd_night_light_get_disabled_until_tmw (nlight)); + + /* disable, with no changes */ + g_settings_set_boolean (settings, "night-light-enabled", FALSE); + g_assert (!gsd_night_light_get_active (nlight)); + g_assert_cmpint (active_cnt, ==, 2); + g_assert_cmpint (sunset_cnt, ==, 1); + g_assert_cmpint (sunrise_cnt, ==, 1); + g_assert_cmpint (temperature_cnt, ==, 4); + g_assert_cmpint (disabled_until_tmw_cnt, ==, 2); + + + /* Finally, check that cancelling a smooth transition works */ + gsd_night_light_set_smooth_enabled (nlight, TRUE); + /* Enable night light and automatic scheduling */ + g_settings_set_boolean (settings, "night-light-schedule-automatic", TRUE); + g_settings_set_boolean (settings, "night-light-enabled", TRUE); + /* It should be active again, and a smooth transition is being done, + * so the color temperature is still the default at this point. */ + g_assert (gsd_night_light_get_active (nlight)); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT); + + /* Turn off immediately, before the first timeout event is fired. */ + g_settings_set_boolean (settings, "night-light-schedule-automatic", FALSE); + g_settings_set_boolean (settings, "night-light-enabled", FALSE); + g_assert (!gsd_night_light_get_active (nlight)); + + /* Now, sleep for a bit (the smooth transition time is 5 seconds) */ + g_timeout_add (5000, quit_mainloop, NULL); + g_main_loop_run (mainloop); + + /* Ensure that the color temperature is still the default one.*/ + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT); + + + /* Check that disabled until tomorrow resets again correctly. */ + g_settings_set_double (settings, "night-light-schedule-from", 17.0); + g_settings_set_double (settings, "night-light-schedule-to", 7.f); + g_settings_set_boolean (settings, "night-light-enabled", TRUE); + gsd_night_light_set_disabled_until_tmw (nlight, TRUE); + + /* Move time past midnight */ + g_clear_pointer (&datetime_override, g_date_time_unref); + datetime_override = g_date_time_new_utc (2017, 2, 9, 1, 0, 0); + gsd_night_light_set_date_time_now (nlight, datetime_override); + g_assert_true (gsd_night_light_get_disabled_until_tmw (nlight)); + + /* Move past sunrise */ + g_clear_pointer (&datetime_override, g_date_time_unref); + datetime_override = g_date_time_new_utc (2017, 2, 9, 8, 0, 0); + gsd_night_light_set_date_time_now (nlight, datetime_override); + g_assert_false (gsd_night_light_get_disabled_until_tmw (nlight)); + + gsd_night_light_set_disabled_until_tmw (nlight, TRUE); + + /* Move into night more than 24h in the future */ + g_clear_pointer (&datetime_override, g_date_time_unref); + datetime_override = g_date_time_new_utc (2017, 2, 10, 20, 0, 0); + gsd_night_light_set_date_time_now (nlight, datetime_override); + g_assert_false (gsd_night_light_get_disabled_until_tmw (nlight)); + + + /* Check that we are always in night mode if from/to are equal. */ + gsd_night_light_set_smooth_enabled (nlight, FALSE); + g_settings_set_double (settings, "night-light-schedule-from", 6.0); + g_settings_set_double (settings, "night-light-schedule-to", 6.0); + g_settings_set_boolean (settings, "night-light-enabled", TRUE); + + datetime_override = g_date_time_new_utc (2017, 2, 10, 5, 50, 0); + gsd_night_light_set_date_time_now (nlight, datetime_override); + g_assert (gsd_night_light_get_active (nlight)); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, 4000); + + datetime_override = g_date_time_new_utc (2017, 2, 10, 6, 0, 0); + gsd_night_light_set_date_time_now (nlight, datetime_override); + g_assert (gsd_night_light_get_active (nlight)); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, 4000); + + datetime_override = g_date_time_new_utc (2017, 2, 10, 6, 10, 0); + gsd_night_light_set_date_time_now (nlight, datetime_override); + g_assert (gsd_night_light_get_active (nlight)); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, 4000); + + + /* Check that the smearing time is lowered correctly when the times are close. */ + g_settings_set_double (settings, "night-light-schedule-from", 6.0); + g_settings_set_double (settings, "night-light-schedule-to", 6.1); + + /* Not enabled 10 minutes before sunset */ + datetime_override = g_date_time_new_utc (2017, 2, 10, 5, 50, 0); + gsd_night_light_set_date_time_now (nlight, datetime_override); + g_assert_false (gsd_night_light_get_active (nlight)); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT); + + /* Not enabled >10 minutes after sunrise */ + datetime_override = g_date_time_new_utc (2017, 2, 10, 6, 20, 0); + gsd_night_light_set_date_time_now (nlight, datetime_override); + g_assert_false (gsd_night_light_get_active (nlight)); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT); + + /* ~50% smeared 3 min before sunrise (sunrise at 6 past) */ + datetime_override = g_date_time_new_utc (2017, 2, 10, 6, 3, 0); + gsd_night_light_set_date_time_now (nlight, datetime_override); + g_assert_true (gsd_night_light_get_active (nlight)); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), <=, (GSD_COLOR_TEMPERATURE_DEFAULT + 4000) / 2 + 20); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), >=, (GSD_COLOR_TEMPERATURE_DEFAULT + 4000) / 2 - 20); + + /* ~50% smeared 3 min before sunset (sunset at 6 past) */ + g_settings_set_double (settings, "night-light-schedule-from", 6.1); + g_settings_set_double (settings, "night-light-schedule-to", 6.0); + datetime_override = g_date_time_new_utc (2017, 2, 10, 6, 3, 0); + gsd_night_light_set_date_time_now (nlight, datetime_override); + g_assert_true (gsd_night_light_get_active (nlight)); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), <=, (GSD_COLOR_TEMPERATURE_DEFAULT + 4000) / 2 + 20); + g_assert_cmpint (gsd_night_light_get_temperature (nlight), >=, (GSD_COLOR_TEMPERATURE_DEFAULT + 4000) / 2 - 20); +} + +static const gboolean +gcm_vendor_is_goldstar (const char * const vendor) { + if (g_strcmp0 (vendor, "Goldstar Company Ltd") == 0) + return TRUE; + /* Goldstar was changed to LG in hwdb (systemd) 240. + * https://github.com/systemd/systemd/commit/c6d7a5e9a3836f8 + */ + if (g_strcmp0 (vendor, "LG Electronics") == 0) + return TRUE; + return FALSE; +} + +static void +gcm_test_edid_func (void) +{ + GcmEdid *edid; + gchar *data; + gboolean ret; + GError *error = NULL; + gsize length = 0; + + edid = gcm_edid_new (); + g_assert (edid != NULL); + + /* LG 21" LCD panel */ + ret = g_file_get_contents (TESTDATADIR "/LG-L225W-External.bin", + &data, &length, &error); + g_assert_no_error (error); + g_assert (ret); + ret = gcm_edid_parse (edid, (const guint8 *) data, length, &error); + g_assert_no_error (error); + g_assert (ret); + + g_assert_cmpstr (gcm_edid_get_monitor_name (edid), ==, "L225W"); + g_assert_true (gcm_vendor_is_goldstar (gcm_edid_get_vendor_name (edid))); + g_assert_cmpstr (gcm_edid_get_serial_number (edid), ==, "34398"); + g_assert_cmpstr (gcm_edid_get_eisa_id (edid), ==, NULL); + g_assert_cmpstr (gcm_edid_get_checksum (edid), ==, "0bb44865bb29984a4bae620656c31368"); + g_assert_cmpstr (gcm_edid_get_pnp_id (edid), ==, "GSM"); + g_assert_cmpint (gcm_edid_get_height (edid), ==, 30); + g_assert_cmpint (gcm_edid_get_width (edid), ==, 47); + g_assert_cmpfloat (gcm_edid_get_gamma (edid), >=, 2.2f - 0.01); + g_assert_cmpfloat (gcm_edid_get_gamma (edid), <, 2.2f + 0.01); + g_free (data); + + /* Lenovo T61 internal Panel */ + ret = g_file_get_contents (TESTDATADIR "/Lenovo-T61-Internal.bin", + &data, &length, &error); + g_assert_no_error (error); + g_assert (ret); + ret = gcm_edid_parse (edid, (const guint8 *) data, length, &error); + g_assert_no_error (error); + g_assert (ret); + + g_assert_cmpstr (gcm_edid_get_monitor_name (edid), ==, NULL); + g_assert_cmpstr (gcm_edid_get_vendor_name (edid), ==, "IBM Brasil"); + g_assert_cmpstr (gcm_edid_get_serial_number (edid), ==, NULL); + g_assert_cmpstr (gcm_edid_get_eisa_id (edid), ==, "LTN154P2-L05"); + g_assert_cmpstr (gcm_edid_get_checksum (edid), ==, "e1865128c7cd5e5ed49ecfc8102f6f9c"); + g_assert_cmpstr (gcm_edid_get_pnp_id (edid), ==, "IBM"); + g_assert_cmpint (gcm_edid_get_height (edid), ==, 21); + g_assert_cmpint (gcm_edid_get_width (edid), ==, 33); + g_assert_cmpfloat (gcm_edid_get_gamma (edid), >=, 2.2f - 0.01); + g_assert_cmpfloat (gcm_edid_get_gamma (edid), <, 2.2f + 0.01); + g_free (data); + + g_object_unref (edid); +} + +static void +gcm_test_sunset_sunrise (void) +{ + gdouble sunrise; + gdouble sunrise_actual = 7.6; + gdouble sunset; + gdouble sunset_actual = 16.8; + g_autoptr(GDateTime) dt = g_date_time_new_utc (2007, 2, 1, 0, 0, 0); + + /* get for London, today */ + gsd_night_light_get_sunrise_sunset (dt, 51.5, -0.1278, &sunrise, &sunset); + g_assert_cmpfloat (sunrise, <, sunrise_actual + 0.1); + g_assert_cmpfloat (sunrise, >, sunrise_actual - 0.1); + g_assert_cmpfloat (sunset, <, sunset_actual + 0.1); + g_assert_cmpfloat (sunset, >, sunset_actual - 0.1); +} + +static void +gcm_test_sunset_sunrise_fractional_timezone (void) +{ + gdouble sunrise; + gdouble sunrise_actual = 7.6 + 1.5; + gdouble sunset; + gdouble sunset_actual = 16.8 + 1.5; + g_autoptr(GTimeZone) tz = NULL; + g_autoptr(GDateTime) dt = NULL; + + tz = g_time_zone_new ("+01:30"); + dt = g_date_time_new (tz, 2007, 2, 1, 0, 0, 0); + + /* get for our made up timezone, today */ + gsd_night_light_get_sunrise_sunset (dt, 51.5, -0.1278, &sunrise, &sunset); + g_assert_cmpfloat (sunrise, <, sunrise_actual + 0.1); + g_assert_cmpfloat (sunrise, >, sunrise_actual - 0.1); + g_assert_cmpfloat (sunset, <, sunset_actual + 0.1); + g_assert_cmpfloat (sunset, >, sunset_actual - 0.1); +} + +static void +gcm_test_frac_day (void) +{ + g_autoptr(GDateTime) dt = g_date_time_new_utc (2007, 2, 1, 12, 59, 59); + gdouble fd; + gdouble fd_actual = 12.99; + + /* test for 12:59:59 */ + fd = gsd_night_light_frac_day_from_dt (dt); + g_assert_cmpfloat (fd, >, fd_actual - 0.01); + g_assert_cmpfloat (fd, <, fd_actual + 0.01); + + /* test same day */ + g_assert_true (gsd_night_light_frac_day_is_between (12, 6, 20)); + g_assert_false (gsd_night_light_frac_day_is_between (5, 6, 20)); + g_assert_true (gsd_night_light_frac_day_is_between (12, 0, 24)); + g_assert_true (gsd_night_light_frac_day_is_between (12, -1, 25)); + + /* test rollover to next day */ + g_assert_true (gsd_night_light_frac_day_is_between (23, 20, 6)); + g_assert_false (gsd_night_light_frac_day_is_between (12, 20, 6)); + + /* test rollover to the previous day */ + g_assert_true (gsd_night_light_frac_day_is_between (5, 16, 8)); + + /* test equality */ + g_assert_true (gsd_night_light_frac_day_is_between (12, 0.5, 24.5)); + g_assert_true (gsd_night_light_frac_day_is_between (0.5, 0.5, 0.5)); +} + +int +main (int argc, char **argv) +{ + g_setenv ("GSETTINGS_BACKEND", "memory", TRUE); + + g_test_init (&argc, &argv, NULL); + + mainloop = g_main_loop_new (g_main_context_default (), FALSE); + + g_test_add_func ("/color/edid", gcm_test_edid_func); + g_test_add_func ("/color/sunset-sunrise", gcm_test_sunset_sunrise); + g_test_add_func ("/color/sunset-sunrise/fractional-timezone", gcm_test_sunset_sunrise_fractional_timezone); + g_test_add_func ("/color/fractional-day", gcm_test_frac_day); + g_test_add_func ("/color/night-light", gcm_test_night_light); + + return g_test_run (); +} + diff --git a/plugins/color/gnome-datetime-source.c b/plugins/color/gnome-datetime-source.c new file mode 100644 index 0000000..287ba2d --- /dev/null +++ b/plugins/color/gnome-datetime-source.c @@ -0,0 +1,286 @@ +/* -*- mode: C; c-file-style: "linux"; indent-tabs-mode: t -*- + * gdatetime-source.c - copy&paste from https://bugzilla.gnome.org/show_bug.cgi?id=655129 + * + * Copyright (C) 2011 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License + * as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * Author: Colin Walters <walters@verbum.org> + */ + +#include "config.h" + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include "gnome-datetime-source.h" + +#if HAVE_TIMERFD +#include <sys/timerfd.h> +#include <unistd.h> +#include <string.h> +#endif + +typedef struct _GDateTimeSource GDateTimeSource; +struct _GDateTimeSource +{ + GSource source; + + gint64 real_expiration; + gint64 wakeup_expiration; + + gboolean cancel_on_set : 1; + gboolean initially_expired : 1; + + GPollFD pollfd; +}; + +static inline void +g_datetime_source_reschedule (GDateTimeSource *datetime_source, + gint64 from_monotonic) +{ + datetime_source->wakeup_expiration = from_monotonic + G_TIME_SPAN_SECOND; +} + +static gboolean +g_datetime_source_is_expired (GDateTimeSource *datetime_source) +{ + gint64 real_now; + gint64 monotonic_now; + + real_now = g_get_real_time (); + monotonic_now = g_source_get_time ((GSource*)datetime_source); + + if (datetime_source->initially_expired) + return TRUE; + + if (datetime_source->real_expiration <= real_now) + return TRUE; + + /* We can't really detect without system support when things + * change; so just trigger every second (i.e. our wakeup + * expiration) + */ + if (datetime_source->cancel_on_set && monotonic_now >= datetime_source->wakeup_expiration) + return TRUE; + + return FALSE; +} + +/* In prepare, we're just checking the monotonic time against + * our projected wakeup. + */ +static gboolean +g_datetime_source_prepare (GSource *source, + gint *timeout) +{ + GDateTimeSource *datetime_source = (GDateTimeSource*)source; + gint64 monotonic_now; + +#if HAVE_TIMERFD + if (datetime_source->pollfd.fd != -1) { + *timeout = -1; + return datetime_source->initially_expired; /* Should be TRUE at most one time, FALSE forever after */ + } +#endif + + monotonic_now = g_source_get_time (source); + + if (monotonic_now < datetime_source->wakeup_expiration) { + /* Round up to ensure that we don't try again too early */ + *timeout = (datetime_source->wakeup_expiration - monotonic_now + 999) / 1000; + return FALSE; + } + + *timeout = 0; + return g_datetime_source_is_expired (datetime_source); +} + +/* In check, we're looking at the wall clock. + */ +static gboolean +g_datetime_source_check (GSource *source) +{ + GDateTimeSource *datetime_source = (GDateTimeSource*)source; + +#if HAVE_TIMERFD + if (datetime_source->pollfd.fd != -1) + return datetime_source->pollfd.revents != 0; +#endif + + if (g_datetime_source_is_expired (datetime_source)) + return TRUE; + + g_datetime_source_reschedule (datetime_source, g_source_get_time (source)); + + return FALSE; +} + +static gboolean +g_datetime_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + GDateTimeSource *datetime_source = (GDateTimeSource*)source; + + datetime_source->initially_expired = FALSE; + + if (!callback) { + g_warning ("Timeout source dispatched without callback\n" + "You must call g_source_set_callback()."); + return FALSE; + } + + (callback) (user_data); + + /* Always false as this source is documented to run once */ + return FALSE; +} + +static void +g_datetime_source_finalize (GSource *source) +{ +#if HAVE_TIMERFD + GDateTimeSource *datetime_source = (GDateTimeSource*)source; + if (datetime_source->pollfd.fd != -1) + close (datetime_source->pollfd.fd); +#endif +} + +static GSourceFuncs g_datetime_source_funcs = { + g_datetime_source_prepare, + g_datetime_source_check, + g_datetime_source_dispatch, + g_datetime_source_finalize +}; + +#if HAVE_TIMERFD +static gboolean +g_datetime_source_init_timerfd (GDateTimeSource *datetime_source, + gint64 expected_now_seconds, + gint64 unix_seconds) +{ + struct itimerspec its; + int settime_flags; + + datetime_source->pollfd.fd = timerfd_create (CLOCK_REALTIME, TFD_CLOEXEC); + if (datetime_source->pollfd.fd == -1) + return FALSE; + + memset (&its, 0, sizeof (its)); + its.it_value.tv_sec = (time_t) unix_seconds; + + /* http://article.gmane.org/gmane.linux.kernel/1132138 */ +#ifndef TFD_TIMER_CANCEL_ON_SET +#define TFD_TIMER_CANCEL_ON_SET (1 << 1) +#endif + + settime_flags = TFD_TIMER_ABSTIME; + if (datetime_source->cancel_on_set) + settime_flags |= TFD_TIMER_CANCEL_ON_SET; + + if (timerfd_settime (datetime_source->pollfd.fd, settime_flags, &its, NULL) < 0) { + close (datetime_source->pollfd.fd); + datetime_source->pollfd.fd = -1; + return FALSE; + } + + /* Now we need to check that the clock didn't go backwards before we + * had the timerfd set up. See + * https://bugzilla.gnome.org/show_bug.cgi?id=655129 + */ + clock_gettime (CLOCK_REALTIME, &its.it_value); + if (its.it_value.tv_sec < expected_now_seconds) + datetime_source->initially_expired = TRUE; + + datetime_source->pollfd.events = G_IO_IN; + + g_source_add_poll ((GSource*) datetime_source, &datetime_source->pollfd); + + return TRUE; +} +#endif + +/** + * _gnome_date_time_source_new: + * @now: The expected current time + * @expiry: Time to await + * @cancel_on_set: Also invoke callback if the system clock changes discontiguously + * + * This function is designed for programs that want to schedule an + * event based on real (wall clock) time, as returned by + * g_get_real_time(). For example, HOUR:MINUTE wall-clock displays + * and calendaring software. The callback will be invoked when the + * specified wall clock time @expiry is reached. This includes + * events such as the system clock being set past the given time. + * + * Compare versus g_timeout_source_new() which is defined to use + * monotonic time as returned by g_get_monotonic_time(). + * + * The parameter @now is necessary to avoid a race condition in + * between getting the current time and calling this function. + * + * If @cancel_on_set is given, the callback will also be invoked at + * most a second after the system clock is changed. This includes + * being set backwards or forwards, and system + * resume from suspend. Not all operating systems allow detecting all + * relevant events efficiently - this function may cause the process + * to wake up once a second in those cases. + * + * A wall clock display should use @cancel_on_set; a calendaring + * program shouldn't need to. + * + * Note that the return value from the associated callback will be + * ignored; this is a one time watch. + * + * <note><para>This function currently does not detect time zone + * changes. On Linux, your program should also monitor the + * <literal>/etc/timezone</literal> file using + * #GFileMonitor.</para></note> + * + * <example id="gdatetime-example-watch"><title>Clock example</title><programlisting><xi:include xmlns:xi="http://www.w3.org/2001/XInclude" parse="text" href="../../../../glib/tests/glib-clock.c"><xi:fallback>FIXME: MISSING XINCLUDE CONTENT</xi:fallback></xi:include></programlisting></example> + * + * Return value: A newly-constructed #GSource + * + * Since: 2.30 + **/ +GSource * +_gnome_datetime_source_new (GDateTime *now, + GDateTime *expiry, + gboolean cancel_on_set) +{ + GDateTimeSource *datetime_source; + gint64 unix_seconds; + + unix_seconds = g_date_time_to_unix (expiry); + + datetime_source = (GDateTimeSource*) g_source_new (&g_datetime_source_funcs, sizeof (GDateTimeSource)); + + datetime_source->cancel_on_set = cancel_on_set; + +#if HAVE_TIMERFD + { + gint64 expected_now_seconds = g_date_time_to_unix (now); + if (g_datetime_source_init_timerfd (datetime_source, expected_now_seconds, unix_seconds)) + return (GSource*)datetime_source; + /* Fall through to non-timerfd code */ + } +#endif + + datetime_source->real_expiration = unix_seconds * 1000000; + g_datetime_source_reschedule (datetime_source, g_get_monotonic_time ()); + + return (GSource*)datetime_source; +} + diff --git a/plugins/color/gnome-datetime-source.h b/plugins/color/gnome-datetime-source.h new file mode 100644 index 0000000..e9ecbf0 --- /dev/null +++ b/plugins/color/gnome-datetime-source.h @@ -0,0 +1,38 @@ +/* gnome-rr.h + * + * Copyright 2011, Red Hat, Inc. + * + * This file is part of the Gnome Library. + * + * The Gnome Library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * The Gnome Library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with the Gnome Library; see the file COPYING.LIB. If not, + * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * Author: Colin Walters <walters@verbum.org> + */ + +#ifndef GNOME_DATETIME_SOURCE_H +#define GNOME_DATETIME_SOURCE_H + +#ifndef GNOME_DESKTOP_USE_UNSTABLE_API +#error This is unstable API. You must define GNOME_DESKTOP_USE_UNSTABLE_API +#endif + +#include <glib.h> + +GSource *_gnome_datetime_source_new (GDateTime *now, + GDateTime *expiry, + gboolean cancel_on_set); + +#endif /* GNOME_DATETIME_SOURCE_H */ diff --git a/plugins/color/gsd-color-calibrate.c b/plugins/color/gsd-color-calibrate.c new file mode 100644 index 0000000..f642e18 --- /dev/null +++ b/plugins/color/gsd-color-calibrate.c @@ -0,0 +1,412 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <colord.h> +#include <libnotify/notify.h> +#include <canberra-gtk.h> + +#include "gsd-color-calibrate.h" + +#define GCM_SESSION_NOTIFY_TIMEOUT 30000 /* ms */ +#define GCM_SETTINGS_RECALIBRATE_PRINTER_THRESHOLD "recalibrate-printer-threshold" +#define GCM_SETTINGS_RECALIBRATE_DISPLAY_THRESHOLD "recalibrate-display-threshold" + +struct _GsdColorCalibrate +{ + GObject parent; + + CdClient *client; + GSettings *settings; +}; + +static void gsd_color_calibrate_class_init (GsdColorCalibrateClass *klass); +static void gsd_color_calibrate_init (GsdColorCalibrate *color_calibrate); +static void gsd_color_calibrate_finalize (GObject *object); + +G_DEFINE_TYPE (GsdColorCalibrate, gsd_color_calibrate, G_TYPE_OBJECT) + +typedef struct { + GsdColorCalibrate *calibrate; + CdProfile *profile; + CdDevice *device; + guint32 output_id; +} GcmSessionAsyncHelper; + +static void +gcm_session_async_helper_free (GcmSessionAsyncHelper *helper) +{ + if (helper->calibrate != NULL) + g_object_unref (helper->calibrate); + if (helper->profile != NULL) + g_object_unref (helper->profile); + if (helper->device != NULL) + g_object_unref (helper->device); + g_free (helper); +} + +static void +gcm_session_exec_control_center (GsdColorCalibrate *calibrate) +{ + gboolean ret; + GError *error = NULL; + GAppInfo *app_info; + GdkAppLaunchContext *launch_context; + + /* setup the launch context so the startup notification is correct */ + launch_context = gdk_display_get_app_launch_context (gdk_display_get_default ()); + app_info = g_app_info_create_from_commandline (BINDIR "/gnome-control-center color", + "gnome-control-center", + G_APP_INFO_CREATE_SUPPORTS_STARTUP_NOTIFICATION, + &error); + if (app_info == NULL) { + g_warning ("failed to create application info: %s", + error->message); + g_error_free (error); + goto out; + } + + /* launch gnome-control-center */ + ret = g_app_info_launch (app_info, + NULL, + G_APP_LAUNCH_CONTEXT (launch_context), + &error); + if (!ret) { + g_warning ("failed to launch gnome-control-center: %s", + error->message); + g_error_free (error); + goto out; + } +out: + g_object_unref (launch_context); + if (app_info != NULL) + g_object_unref (app_info); +} + +static void +gcm_session_notify_cb (NotifyNotification *notification, + gchar *action, + gpointer user_data) +{ + GsdColorCalibrate *calibrate = GSD_COLOR_CALIBRATE (user_data); + + if (g_strcmp0 (action, "recalibrate") == 0) { + notify_notification_close (notification, NULL); + gcm_session_exec_control_center (calibrate); + } +} + +static void +closed_cb (NotifyNotification *notification, gpointer data) +{ + g_object_unref (notification); +} + +static gboolean +gcm_session_notify_recalibrate (GsdColorCalibrate *calibrate, + const gchar *title, + const gchar *message, + CdDeviceKind kind) +{ + gboolean ret; + GError *error = NULL; + NotifyNotification *notification; + + /* show a bubble */ + notification = notify_notification_new (title, message, "preferences-color"); + notify_notification_set_timeout (notification, GCM_SESSION_NOTIFY_TIMEOUT); + notify_notification_set_urgency (notification, NOTIFY_URGENCY_LOW); + notify_notification_set_app_name (notification, _("Color")); + notify_notification_set_hint_string (notification, "desktop-entry", "gnome-color-panel"); + + notify_notification_add_action (notification, + "recalibrate", + /* TRANSLATORS: button: this is to open GCM */ + _("Recalibrate now"), + gcm_session_notify_cb, + calibrate, NULL); + + g_signal_connect (notification, "closed", G_CALLBACK (closed_cb), NULL); + ret = notify_notification_show (notification, &error); + if (!ret) { + g_warning ("failed to show notification: %s", + error->message); + g_error_free (error); + } + return ret; +} + +static gchar * +gcm_session_device_get_title (CdDevice *device) +{ + const gchar *vendor; + const gchar *model; + + model = cd_device_get_model (device); + vendor = cd_device_get_vendor (device); + if (model != NULL && vendor != NULL) + return g_strdup_printf ("%s - %s", vendor, model); + if (vendor != NULL) + return g_strdup (vendor); + if (model != NULL) + return g_strdup (model); + return g_strdup (cd_device_get_id (device)); +} + +static void +gcm_session_notify_device (GsdColorCalibrate *calibrate, CdDevice *device) +{ + CdDeviceKind kind; + const gchar *title; + gchar *device_title = NULL; + gchar *message; + guint threshold; + glong since; + + /* TRANSLATORS: this is when the device has not been recalibrated in a while */ + title = _("Recalibration required"); + device_title = gcm_session_device_get_title (device); + + /* check we care */ + kind = cd_device_get_kind (device); + if (kind == CD_DEVICE_KIND_DISPLAY) { + + /* get from GSettings */ + threshold = g_settings_get_uint (calibrate->settings, + GCM_SETTINGS_RECALIBRATE_DISPLAY_THRESHOLD); + + /* TRANSLATORS: this is when the display has not been recalibrated in a while */ + message = g_strdup_printf (_("The display “%s” should be recalibrated soon."), + device_title); + } else { + + /* get from GSettings */ + threshold = g_settings_get_uint (calibrate->settings, + GCM_SETTINGS_RECALIBRATE_PRINTER_THRESHOLD); + + /* TRANSLATORS: this is when the printer has not been recalibrated in a while */ + message = g_strdup_printf (_("The printer “%s” should be recalibrated soon."), + device_title); + } + + /* check if we need to notify */ + since = (g_get_real_time () - cd_device_get_modified (device)) / G_USEC_PER_SEC; + if (threshold > since) + gcm_session_notify_recalibrate (calibrate, title, message, kind); + g_free (device_title); + g_free (message); +} + +static void +gcm_session_profile_connect_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + const gchar *filename; + gboolean ret; + gchar *basename = NULL; + const gchar *data_source; + GError *error = NULL; + CdProfile *profile = CD_PROFILE (object); + GcmSessionAsyncHelper *helper = (GcmSessionAsyncHelper *) user_data; + GsdColorCalibrate *calibrate = GSD_COLOR_CALIBRATE (helper->calibrate); + + ret = cd_profile_connect_finish (profile, + res, + &error); + if (!ret) { + g_warning ("failed to connect to profile: %s", + error->message); + g_error_free (error); + goto out; + } + + /* ensure it's a profile generated by us */ + data_source = cd_profile_get_metadata_item (profile, + CD_PROFILE_METADATA_DATA_SOURCE); + if (data_source == NULL) { + + /* existing profiles from gnome-color-calibrate < 3.1 + * won't have the extra metadata values added */ + filename = cd_profile_get_filename (profile); + if (filename == NULL) + goto out; + basename = g_path_get_basename (filename); + if (!g_str_has_prefix (basename, "GCM")) { + g_debug ("not a GCM profile for %s: %s", + cd_device_get_id (helper->device), filename); + goto out; + } + + /* ensure it's been created from a calibration, rather than from + * auto-EDID */ + } else if (g_strcmp0 (data_source, + CD_PROFILE_METADATA_DATA_SOURCE_CALIB) != 0) { + g_debug ("not a calib profile for %s", + cd_device_get_id (helper->device)); + goto out; + } + + /* handle device */ + gcm_session_notify_device (calibrate, helper->device); +out: + gcm_session_async_helper_free (helper); + g_free (basename); +} + +static void +gcm_session_device_connect_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + gboolean ret; + GError *error = NULL; + CdDeviceKind kind; + CdProfile *profile = NULL; + CdDevice *device = CD_DEVICE (object); + GsdColorCalibrate *calibrate = GSD_COLOR_CALIBRATE (user_data); + GcmSessionAsyncHelper *helper; + + ret = cd_device_connect_finish (device, + res, + &error); + if (!ret) { + g_warning ("failed to connect to device: %s", + error->message); + g_error_free (error); + goto out; + } + + /* check we care */ + kind = cd_device_get_kind (device); + if (kind != CD_DEVICE_KIND_DISPLAY && + kind != CD_DEVICE_KIND_PRINTER) + goto out; + + /* ensure we have a profile */ + profile = cd_device_get_default_profile (device); + if (profile == NULL) { + g_debug ("no profile set for %s", cd_device_get_id (device)); + goto out; + } + + /* connect to the profile */ + helper = g_new0 (GcmSessionAsyncHelper, 1); + helper->calibrate = g_object_ref (calibrate); + helper->device = g_object_ref (device); + cd_profile_connect (profile, + NULL, + gcm_session_profile_connect_cb, + helper); +out: + if (profile != NULL) + g_object_unref (profile); +} + +static void +gcm_session_device_added_notify_cb (CdClient *client, + CdDevice *device, + GsdColorCalibrate *calibrate) +{ + /* connect to the device to get properties */ + cd_device_connect (device, + NULL, + gcm_session_device_connect_cb, + calibrate); +} + +static void +gcm_session_sensor_added_cb (CdClient *client, + CdSensor *sensor, + GsdColorCalibrate *calibrate) +{ + ca_context_play (ca_gtk_context_get (), 0, + CA_PROP_EVENT_ID, "device-added", + /* TRANSLATORS: this is the application name */ + CA_PROP_APPLICATION_NAME, _("GNOME Settings Daemon Color Plugin"), + /* TRANSLATORS: this is a sound description */ + CA_PROP_EVENT_DESCRIPTION, _("Color calibration device added"), NULL); + + /* open up the color prefs window */ + gcm_session_exec_control_center (calibrate); +} + +static void +gcm_session_sensor_removed_cb (CdClient *client, + CdSensor *sensor, + GsdColorCalibrate *calibrate) +{ + ca_context_play (ca_gtk_context_get (), 0, + CA_PROP_EVENT_ID, "device-removed", + /* TRANSLATORS: this is the application name */ + CA_PROP_APPLICATION_NAME, _("GNOME Settings Daemon Color Plugin"), + /* TRANSLATORS: this is a sound description */ + CA_PROP_EVENT_DESCRIPTION, _("Color calibration device removed"), NULL); +} + +static void +gsd_color_calibrate_class_init (GsdColorCalibrateClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_color_calibrate_finalize; +} + +static void +gsd_color_calibrate_init (GsdColorCalibrate *calibrate) +{ + calibrate->settings = g_settings_new ("org.gnome.settings-daemon.plugins.color"); + calibrate->client = cd_client_new (); + g_signal_connect (calibrate->client, "device-added", + G_CALLBACK (gcm_session_device_added_notify_cb), + calibrate); + g_signal_connect (calibrate->client, "sensor-added", + G_CALLBACK (gcm_session_sensor_added_cb), + calibrate); + g_signal_connect (calibrate->client, "sensor-removed", + G_CALLBACK (gcm_session_sensor_removed_cb), + calibrate); +} + +static void +gsd_color_calibrate_finalize (GObject *object) +{ + GsdColorCalibrate *calibrate; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_COLOR_CALIBRATE (object)); + + calibrate = GSD_COLOR_CALIBRATE (object); + + g_clear_object (&calibrate->settings); + g_clear_object (&calibrate->client); + + G_OBJECT_CLASS (gsd_color_calibrate_parent_class)->finalize (object); +} + +GsdColorCalibrate * +gsd_color_calibrate_new (void) +{ + GsdColorCalibrate *calibrate; + calibrate = g_object_new (GSD_TYPE_COLOR_CALIBRATE, NULL); + return GSD_COLOR_CALIBRATE (calibrate); +} diff --git a/plugins/color/gsd-color-calibrate.h b/plugins/color/gsd-color-calibrate.h new file mode 100644 index 0000000..8980098 --- /dev/null +++ b/plugins/color/gsd-color-calibrate.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_COLOR_CALIBRATE_H +#define __GSD_COLOR_CALIBRATE_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_COLOR_CALIBRATE (gsd_color_calibrate_get_type ()) +G_DECLARE_FINAL_TYPE (GsdColorCalibrate, gsd_color_calibrate, GSD, COLOR_CALIBRATE, GObject) + +GType gsd_color_calibrate_get_type (void); +GQuark gsd_color_calibrate_error_quark (void); + +GsdColorCalibrate * gsd_color_calibrate_new (void); + +G_END_DECLS + +#endif /* __GSD_COLOR_CALIBRATE_H */ diff --git a/plugins/color/gsd-color-manager.c b/plugins/color/gsd-color-manager.c new file mode 100644 index 0000000..62a417e --- /dev/null +++ b/plugins/color/gsd-color-manager.c @@ -0,0 +1,502 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gdk/gdk.h> +#include <gtk/gtk.h> + +#include "gnome-settings-profile.h" +#include "gsd-color-calibrate.h" +#include "gsd-color-manager.h" +#include "gsd-color-profiles.h" +#include "gsd-color-state.h" +#include "gsd-night-light.h" + +#define GSD_DBUS_NAME "org.gnome.SettingsDaemon" +#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon" +#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon" + +#define GSD_COLOR_DBUS_NAME GSD_DBUS_NAME ".Color" +#define GSD_COLOR_DBUS_PATH GSD_DBUS_PATH "/Color" +#define GSD_COLOR_DBUS_INTERFACE GSD_DBUS_BASE_INTERFACE ".Color" + +static const gchar introspection_xml[] = +"<node>" +" <interface name='org.gnome.SettingsDaemon.Color'>" +" <method name='NightLightPreview'>" +" <arg type='u' name='duration' direction='in'/>" +" </method>" +" <property name='NightLightActive' type='b' access='read'/>" +" <property name='Temperature' type='u' access='readwrite'/>" +" <property name='DisabledUntilTomorrow' type='b' access='readwrite'/>" +" <property name='Sunrise' type='d' access='read'/>" +" <property name='Sunset' type='d' access='read'/>" +" </interface>" +"</node>"; + +struct _GsdColorManager +{ + GObject parent; + + /* D-Bus */ + guint name_id; + GDBusNodeInfo *introspection_data; + GDBusConnection *connection; + GCancellable *bus_cancellable; + + GsdColorCalibrate *calibrate; + GsdColorProfiles *profiles; + GsdColorState *state; + GsdNightLight *nlight; + + guint nlight_forced_timeout_id; +}; + +enum { + PROP_0, +}; + +static void gsd_color_manager_class_init (GsdColorManagerClass *klass); +static void gsd_color_manager_init (GsdColorManager *color_manager); +static void gsd_color_manager_finalize (GObject *object); + +G_DEFINE_TYPE (GsdColorManager, gsd_color_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +GQuark +gsd_color_manager_error_quark (void) +{ + static GQuark quark = 0; + if (!quark) + quark = g_quark_from_static_string ("gsd_color_manager_error"); + return quark; +} + +gboolean +gsd_color_manager_start (GsdColorManager *manager, + GError **error) +{ + gboolean ret; + + g_debug ("Starting color manager"); + gnome_settings_profile_start (NULL); + + /* start the device probing */ + gsd_color_state_start (manager->state); + + /* start the profiles collection */ + ret = gsd_color_profiles_start (manager->profiles, error); + if (!ret) + goto out; +out: + gnome_settings_profile_end (NULL); + return ret; +} + +void +gsd_color_manager_stop (GsdColorManager *manager) +{ + g_debug ("Stopping color manager"); + gsd_color_state_stop (manager->state); + gsd_color_profiles_stop (manager->profiles); +} + +static void +gsd_color_manager_class_init (GsdColorManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_color_manager_finalize; +} + +static void +emit_property_changed (GsdColorManager *manager, + const gchar *property_name, + GVariant *property_value) +{ + GVariantBuilder builder; + GVariantBuilder invalidated_builder; + + /* not yet connected */ + if (manager->connection == NULL) + return; + + /* build the dict */ + g_variant_builder_init (&invalidated_builder, G_VARIANT_TYPE ("as")); + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + g_variant_builder_add (&builder, + "{sv}", + property_name, + property_value); + g_dbus_connection_emit_signal (manager->connection, + NULL, + GSD_COLOR_DBUS_PATH, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new ("(sa{sv}as)", + GSD_COLOR_DBUS_INTERFACE, + &builder, + &invalidated_builder), + NULL); + g_variant_builder_clear (&builder); + g_variant_builder_clear (&invalidated_builder); +} + +static void +on_active_notify (GsdNightLight *nlight, + GParamSpec *pspec, + gpointer user_data) +{ + GsdColorManager *manager = GSD_COLOR_MANAGER (user_data); + emit_property_changed (manager, "NightLightActive", + g_variant_new_boolean (gsd_night_light_get_active (manager->nlight))); +} + +static void +on_sunset_notify (GsdNightLight *nlight, + GParamSpec *pspec, + gpointer user_data) +{ + GsdColorManager *manager = GSD_COLOR_MANAGER (user_data); + emit_property_changed (manager, "Sunset", + g_variant_new_double (gsd_night_light_get_sunset (manager->nlight))); +} + +static void +on_sunrise_notify (GsdNightLight *nlight, + GParamSpec *pspec, + gpointer user_data) +{ + GsdColorManager *manager = GSD_COLOR_MANAGER (user_data); + emit_property_changed (manager, "Sunrise", + g_variant_new_double (gsd_night_light_get_sunrise (manager->nlight))); +} + +static void +on_disabled_until_tmw_notify (GsdNightLight *nlight, + GParamSpec *pspec, + gpointer user_data) +{ + GsdColorManager *manager = GSD_COLOR_MANAGER (user_data); + emit_property_changed (manager, "DisabledUntilTomorrow", + g_variant_new_boolean (gsd_night_light_get_disabled_until_tmw (manager->nlight))); +} + +static void +on_temperature_notify (GsdNightLight *nlight, + GParamSpec *pspec, + gpointer user_data) +{ + GsdColorManager *manager = GSD_COLOR_MANAGER (user_data); + gdouble temperature = gsd_night_light_get_temperature (manager->nlight); + gsd_color_state_set_temperature (manager->state, temperature); + emit_property_changed (manager, "Temperature", + g_variant_new_double (temperature)); +} + +static void +gsd_color_manager_init (GsdColorManager *manager) +{ + /* setup calibration features */ + manager->calibrate = gsd_color_calibrate_new (); + manager->profiles = gsd_color_profiles_new (); + manager->state = gsd_color_state_new (); + + /* night light features */ + manager->nlight = gsd_night_light_new (); + g_signal_connect (manager->nlight, "notify::active", + G_CALLBACK (on_active_notify), manager); + g_signal_connect (manager->nlight, "notify::sunset", + G_CALLBACK (on_sunset_notify), manager); + g_signal_connect (manager->nlight, "notify::sunrise", + G_CALLBACK (on_sunrise_notify), manager); + g_signal_connect (manager->nlight, "notify::temperature", + G_CALLBACK (on_temperature_notify), manager); + g_signal_connect (manager->nlight, "notify::disabled-until-tmw", + G_CALLBACK (on_disabled_until_tmw_notify), manager); +} + +static void +gsd_color_manager_finalize (GObject *object) +{ + GsdColorManager *manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_COLOR_MANAGER (object)); + + manager = GSD_COLOR_MANAGER (object); + + gsd_color_manager_stop (manager); + + if (manager->bus_cancellable != NULL) { + g_cancellable_cancel (manager->bus_cancellable); + g_clear_object (&manager->bus_cancellable); + } + + g_clear_pointer (&manager->introspection_data, g_dbus_node_info_unref); + g_clear_object (&manager->connection); + + if (manager->name_id != 0) { + g_bus_unown_name (manager->name_id); + manager->name_id = 0; + } + + if (manager->nlight_forced_timeout_id) + g_source_remove (manager->nlight_forced_timeout_id); + + g_clear_object (&manager->calibrate); + g_clear_object (&manager->profiles); + g_clear_object (&manager->state); + g_clear_object (&manager->nlight); + + G_OBJECT_CLASS (gsd_color_manager_parent_class)->finalize (object); +} + +static gboolean +nlight_forced_timeout_cb (gpointer user_data) +{ + GsdColorManager *manager = GSD_COLOR_MANAGER (user_data); + + manager->nlight_forced_timeout_id = 0; + gsd_night_light_set_forced (manager->nlight, FALSE); + + return G_SOURCE_REMOVE; +} + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GsdColorManager *manager = GSD_COLOR_MANAGER (user_data); + + if (g_strcmp0 (method_name, "NightLightPreview") == 0) { + guint32 duration = 0; + + if (!manager->nlight) { + g_dbus_method_invocation_return_error_literal (invocation, + G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED, + "Night-light is currently unavailable"); + + return; + } + + g_variant_get (parameters, "(u)", &duration); + + if (duration == 0 || duration > 120) { + g_dbus_method_invocation_return_error_literal (invocation, + G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Duration is out of the range (0-120]."); + + return; + } + + if (manager->nlight_forced_timeout_id) + g_source_remove (manager->nlight_forced_timeout_id); + manager->nlight_forced_timeout_id = g_timeout_add_seconds (duration, nlight_forced_timeout_cb, manager); + + gsd_night_light_set_forced (manager->nlight, TRUE); + + g_dbus_method_invocation_return_value (invocation, NULL); + } else { + g_assert_not_reached (); + } +} + +static GVariant * +handle_get_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, gpointer user_data) +{ + GsdColorManager *manager = GSD_COLOR_MANAGER (user_data); + + if (g_strcmp0 (interface_name, GSD_COLOR_DBUS_INTERFACE) != 0) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "No such interface: %s", interface_name); + return NULL; + } + + if (g_strcmp0 (property_name, "NightLightActive") == 0) + return g_variant_new_boolean (gsd_night_light_get_active (manager->nlight)); + + if (g_strcmp0 (property_name, "Temperature") == 0) { + guint temperature; + temperature = gsd_color_state_get_temperature (manager->state); + return g_variant_new_uint32 (temperature); + } + + if (g_strcmp0 (property_name, "DisabledUntilTomorrow") == 0) + return g_variant_new_boolean (gsd_night_light_get_disabled_until_tmw (manager->nlight)); + + if (g_strcmp0 (property_name, "Sunrise") == 0) + return g_variant_new_double (gsd_night_light_get_sunrise (manager->nlight)); + + if (g_strcmp0 (property_name, "Sunset") == 0) + return g_variant_new_double (gsd_night_light_get_sunset (manager->nlight)); + + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Failed to get property: %s", property_name); + return NULL; +} + +static gboolean +handle_set_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GVariant *value, + GError **error, gpointer user_data) +{ + GsdColorManager *manager = GSD_COLOR_MANAGER (user_data); + + if (g_strcmp0 (interface_name, GSD_COLOR_DBUS_INTERFACE) != 0) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "No such interface: %s", interface_name); + return FALSE; + } + + if (g_strcmp0 (property_name, "Temperature") == 0) { + guint32 temperature; + g_variant_get (value, "u", &temperature); + if (temperature < GSD_COLOR_TEMPERATURE_MIN) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "%" G_GUINT32_FORMAT "K is < min %" G_GUINT32_FORMAT "K", + temperature, GSD_COLOR_TEMPERATURE_MIN); + return FALSE; + } + if (temperature > GSD_COLOR_TEMPERATURE_MAX) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "%" G_GUINT32_FORMAT "K is > max %" G_GUINT32_FORMAT "K", + temperature, GSD_COLOR_TEMPERATURE_MAX); + return FALSE; + } + gsd_color_state_set_temperature (manager->state, temperature); + return TRUE; + } + + if (g_strcmp0 (property_name, "DisabledUntilTomorrow") == 0) { + gsd_night_light_set_disabled_until_tmw (manager->nlight, + g_variant_get_boolean (value)); + return TRUE; + } + + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "No such property: %s", property_name); + return FALSE; +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + handle_get_property, + handle_set_property +}; + +static void +name_lost_handler_cb (GDBusConnection *connection, const gchar *name, gpointer user_data) +{ + g_debug ("lost name, so exiting"); + gtk_main_quit (); +} + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *res, + GsdColorManager *manager) +{ + GDBusConnection *connection; + GError *error = NULL; + + connection = g_bus_get_finish (res, &error); + if (connection == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Could not get session bus: %s", error->message); + g_error_free (error); + return; + } + + manager->connection = connection; + + g_dbus_connection_register_object (connection, + GSD_COLOR_DBUS_PATH, + manager->introspection_data->interfaces[0], + &interface_vtable, + manager, + NULL, + NULL); + + manager->name_id = g_bus_own_name_on_connection (connection, + GSD_COLOR_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, + name_lost_handler_cb, + manager, + NULL); + + /* setup night light module */ + if (!gsd_night_light_start (manager->nlight, &error)) { + g_warning ("Could not start night light module: %s", error->message); + g_error_free (error); + } +} + +static void +register_manager_dbus (GsdColorManager *manager) +{ + manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + g_assert (manager->introspection_data != NULL); + manager->bus_cancellable = g_cancellable_new (); + + g_bus_get (G_BUS_TYPE_SESSION, + manager->bus_cancellable, + (GAsyncReadyCallback) on_bus_gotten, + manager); +} + +GsdColorManager * +gsd_color_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_COLOR_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + register_manager_dbus (manager_object); + } + + return GSD_COLOR_MANAGER (manager_object); +} diff --git a/plugins/color/gsd-color-manager.h b/plugins/color/gsd-color-manager.h new file mode 100644 index 0000000..ba71fca --- /dev/null +++ b/plugins/color/gsd-color-manager.h @@ -0,0 +1,46 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2011 Richard Hughes <richard@hughsie.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_COLOR_MANAGER_H +#define __GSD_COLOR_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_COLOR_MANAGER (gsd_color_manager_get_type ()) +#define GSD_COLOR_MANAGER_ERROR (gsd_color_manager_error_quark ()) +G_DECLARE_FINAL_TYPE (GsdColorManager, gsd_color_manager, GSD, COLOR_MANAGER, GObject) + +enum +{ + GSD_COLOR_MANAGER_ERROR_FAILED +}; + +GQuark gsd_color_manager_error_quark (void); + +GsdColorManager * gsd_color_manager_new (void); +gboolean gsd_color_manager_start (GsdColorManager *manager, + GError **error); +void gsd_color_manager_stop (GsdColorManager *manager); + +G_END_DECLS + +#endif /* __GSD_COLOR_MANAGER_H */ diff --git a/plugins/color/gsd-color-profiles.c b/plugins/color/gsd-color-profiles.c new file mode 100644 index 0000000..c73abd7 --- /dev/null +++ b/plugins/color/gsd-color-profiles.c @@ -0,0 +1,246 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <colord.h> + +#include "gsd-color-profiles.h" + +struct _GsdColorProfiles +{ + GObject parent; + + GCancellable *cancellable; + CdClient *client; + CdIccStore *icc_store; +}; + +static void gsd_color_profiles_class_init (GsdColorProfilesClass *klass); +static void gsd_color_profiles_init (GsdColorProfiles *color_profiles); +static void gsd_color_profiles_finalize (GObject *object); + +G_DEFINE_TYPE (GsdColorProfiles, gsd_color_profiles, G_TYPE_OBJECT) + +static void +gsd_color_profiles_class_init (GsdColorProfilesClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_color_profiles_finalize; +} + +static void +gcm_session_client_connect_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + gboolean ret; + GError *error = NULL; + CdClient *client = CD_CLIENT (source_object); + GsdColorProfiles *profiles; + + /* connected */ + ret = cd_client_connect_finish (client, res, &error); + if (!ret) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("failed to connect to colord: %s", error->message); + g_error_free (error); + return; + } + + /* is there an available colord instance? */ + profiles = GSD_COLOR_PROFILES (user_data); + ret = cd_client_get_has_server (profiles->client); + if (!ret) { + g_warning ("There is no colord server available"); + return; + } + + /* add profiles */ + ret = cd_icc_store_search_kind (profiles->icc_store, + CD_ICC_STORE_SEARCH_KIND_USER, + CD_ICC_STORE_SEARCH_FLAGS_CREATE_LOCATION, + profiles->cancellable, + &error); + if (!ret) { + g_warning ("failed to add user icc: %s", error->message); + g_error_free (error); + } +} + +gboolean +gsd_color_profiles_start (GsdColorProfiles *profiles, + GError **error) +{ + /* use a fresh cancellable for each start->stop operation */ + g_cancellable_cancel (profiles->cancellable); + g_clear_object (&profiles->cancellable); + profiles->cancellable = g_cancellable_new (); + + cd_client_connect (profiles->client, + profiles->cancellable, + gcm_session_client_connect_cb, + profiles); + + return TRUE; +} + +void +gsd_color_profiles_stop (GsdColorProfiles *profiles) +{ + g_cancellable_cancel (profiles->cancellable); +} + +static void +gcm_session_create_profile_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + CdProfile *profile; + GError *error = NULL; + CdClient *client = CD_CLIENT (object); + + profile = cd_client_create_profile_finish (client, res, &error); + if (profile == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, CD_CLIENT_ERROR, CD_CLIENT_ERROR_ALREADY_EXISTS)) + g_warning ("%s", error->message); + g_error_free (error); + return; + } + g_object_unref (profile); +} + +static void +gcm_session_icc_store_added_cb (CdIccStore *icc_store, + CdIcc *icc, + GsdColorProfiles *profiles) +{ + cd_client_create_profile_for_icc (profiles->client, + icc, + CD_OBJECT_SCOPE_TEMP, + profiles->cancellable, + gcm_session_create_profile_cb, + profiles); +} + +static void +gcm_session_delete_profile_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + gboolean ret; + GError *error = NULL; + CdClient *client = CD_CLIENT (object); + + ret = cd_client_delete_profile_finish (client, res, &error); + if (!ret) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("%s", error->message); + g_error_free (error); + } +} + +static void +gcm_session_find_profile_by_filename_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + GError *error = NULL; + CdProfile *profile; + CdClient *client = CD_CLIENT (object); + GsdColorProfiles *profiles = GSD_COLOR_PROFILES (user_data); + + profile = cd_client_find_profile_by_filename_finish (client, res, &error); + if (profile == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("%s", error->message); + g_error_free (error); + goto out; + } + + /* remove it from colord */ + cd_client_delete_profile (profiles->client, + profile, + profiles->cancellable, + gcm_session_delete_profile_cb, + profiles); +out: + if (profile != NULL) + g_object_unref (profile); +} + +static void +gcm_session_icc_store_removed_cb (CdIccStore *icc_store, + CdIcc *icc, + GsdColorProfiles *profiles) +{ + /* find the ID for the filename */ + g_debug ("filename %s removed", cd_icc_get_filename (icc)); + cd_client_find_profile_by_filename (profiles->client, + cd_icc_get_filename (icc), + profiles->cancellable, + gcm_session_find_profile_by_filename_cb, + profiles); +} + +static void +gsd_color_profiles_init (GsdColorProfiles *profiles) +{ + /* have access to all user profiles */ + profiles->client = cd_client_new (); + profiles->icc_store = cd_icc_store_new (); + cd_icc_store_set_load_flags (profiles->icc_store, + CD_ICC_LOAD_FLAGS_FALLBACK_MD5); + g_signal_connect (profiles->icc_store, "added", + G_CALLBACK (gcm_session_icc_store_added_cb), + profiles); + g_signal_connect (profiles->icc_store, "removed", + G_CALLBACK (gcm_session_icc_store_removed_cb), + profiles); +} + +static void +gsd_color_profiles_finalize (GObject *object) +{ + GsdColorProfiles *profiles; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_COLOR_PROFILES (object)); + + profiles = GSD_COLOR_PROFILES (object); + + g_cancellable_cancel (profiles->cancellable); + g_clear_object (&profiles->cancellable); + g_clear_object (&profiles->icc_store); + g_clear_object (&profiles->client); + + G_OBJECT_CLASS (gsd_color_profiles_parent_class)->finalize (object); +} + +GsdColorProfiles * +gsd_color_profiles_new (void) +{ + GsdColorProfiles *profiles; + profiles = g_object_new (GSD_TYPE_COLOR_PROFILES, NULL); + return GSD_COLOR_PROFILES (profiles); +} diff --git a/plugins/color/gsd-color-profiles.h b/plugins/color/gsd-color-profiles.h new file mode 100644 index 0000000..24aed34 --- /dev/null +++ b/plugins/color/gsd-color-profiles.h @@ -0,0 +1,40 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_COLOR_PROFILES_H +#define __GSD_COLOR_PROFILES_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_COLOR_PROFILES (gsd_color_profiles_get_type ()) +G_DECLARE_FINAL_TYPE (GsdColorProfiles, gsd_color_profiles, GSD, COLOR_PROFILES, GObject) + +GQuark gsd_color_profiles_error_quark (void); + +GsdColorProfiles * gsd_color_profiles_new (void); +gboolean gsd_color_profiles_start (GsdColorProfiles *profiles, + GError **error); +void gsd_color_profiles_stop (GsdColorProfiles *profiles); + +G_END_DECLS + +#endif /* __GSD_COLOR_PROFILES_H */ diff --git a/plugins/color/gsd-color-state.c b/plugins/color/gsd-color-state.c new file mode 100644 index 0000000..746cf23 --- /dev/null +++ b/plugins/color/gsd-color-state.c @@ -0,0 +1,1592 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <colord.h> +#include <gdk/gdk.h> +#include <stdlib.h> +#include <lcms2.h> +#include <canberra-gtk.h> + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include <libgnome-desktop/gnome-rr.h> + +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#endif + +#include "gnome-settings-bus.h" + +#include "gsd-color-manager.h" +#include "gsd-color-state.h" +#include "gcm-edid.h" + +#define GSD_DBUS_NAME "org.gnome.SettingsDaemon" +#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon" +#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon" + +static void gcm_session_set_gamma_for_all_devices (GsdColorState *state); + +struct _GsdColorState +{ + GObject parent; + + GCancellable *cancellable; + GsdSessionManager *session; + CdClient *client; + GnomeRRScreen *state_screen; + GHashTable *edid_cache; + GdkWindow *gdk_window; + gboolean session_is_active; + GHashTable *device_assign_hash; + guint color_temperature; +}; + +static void gsd_color_state_class_init (GsdColorStateClass *klass); +static void gsd_color_state_init (GsdColorState *color_state); +static void gsd_color_state_finalize (GObject *object); + +G_DEFINE_TYPE (GsdColorState, gsd_color_state, G_TYPE_OBJECT) + +/* see http://www.oyranos.org/wiki/index.php?title=ICC_Profiles_in_X_Specification_0.3 */ +#define GCM_ICC_PROFILE_IN_X_VERSION_MAJOR 0 +#define GCM_ICC_PROFILE_IN_X_VERSION_MINOR 3 + +typedef struct { + guint32 red; + guint32 green; + guint32 blue; +} GnomeRROutputClutItem; + +GQuark +gsd_color_state_error_quark (void) +{ + static GQuark quark = 0; + if (!quark) + quark = g_quark_from_static_string ("gsd_color_state_error"); + return quark; +} + +static GcmEdid * +gcm_session_get_output_edid (GsdColorState *state, GnomeRROutput *output, GError **error) +{ + const guint8 *data; + gsize size; + GcmEdid *edid = NULL; + gboolean ret; + + /* can we find it in the cache */ + edid = g_hash_table_lookup (state->edid_cache, + gnome_rr_output_get_name (output)); + if (edid != NULL) { + g_object_ref (edid); + return edid; + } + + /* parse edid */ + data = gnome_rr_output_get_edid_data (output, &size); + if (data == NULL || size == 0) { + g_set_error_literal (error, + GNOME_RR_ERROR, + GNOME_RR_ERROR_UNKNOWN, + "unable to get EDID for output"); + return NULL; + } + edid = gcm_edid_new (); + ret = gcm_edid_parse (edid, data, size, error); + if (!ret) { + g_object_unref (edid); + return NULL; + } + + /* add to cache */ + g_hash_table_insert (state->edid_cache, + g_strdup (gnome_rr_output_get_name (output)), + g_object_ref (edid)); + + return edid; +} + +static gboolean +gcm_session_screen_set_icc_profile (GsdColorState *state, + const gchar *filename, + GError **error) +{ + gchar *data = NULL; + gsize length; + guint version_data; + + g_return_val_if_fail (filename != NULL, FALSE); + + /* wayland */ + if (state->gdk_window == NULL) { + g_debug ("not setting atom as running under wayland"); + return TRUE; + } + + g_debug ("setting root window ICC profile atom from %s", filename); + + /* get contents of file */ + if (!g_file_get_contents (filename, &data, &length, error)) + return FALSE; + + /* set profile property */ + gdk_property_change (state->gdk_window, + gdk_atom_intern_static_string ("_ICC_PROFILE"), + gdk_atom_intern_static_string ("CARDINAL"), + 8, + GDK_PROP_MODE_REPLACE, + (const guchar *) data, length); + + /* set version property */ + version_data = GCM_ICC_PROFILE_IN_X_VERSION_MAJOR * 100 + + GCM_ICC_PROFILE_IN_X_VERSION_MINOR * 1; + gdk_property_change (state->gdk_window, + gdk_atom_intern_static_string ("_ICC_PROFILE_IN_X_VERSION"), + gdk_atom_intern_static_string ("CARDINAL"), + 8, + GDK_PROP_MODE_REPLACE, + (const guchar *) &version_data, 1); + + g_free (data); + return TRUE; +} + +void +gsd_color_state_set_temperature (GsdColorState *state, guint temperature) +{ + g_return_if_fail (GSD_IS_COLOR_STATE (state)); + + if (state->color_temperature == temperature) + return; + + state->color_temperature = temperature; + gcm_session_set_gamma_for_all_devices (state); +} + +guint +gsd_color_state_get_temperature (GsdColorState *state) +{ + g_return_val_if_fail (GSD_IS_COLOR_STATE (state), 0); + return state->color_temperature; +} + +static gchar * +gcm_session_get_output_id (GsdColorState *state, GnomeRROutput *output) +{ + const gchar *name; + const gchar *serial; + const gchar *vendor; + GcmEdid *edid = NULL; + GString *device_id; + GError *error = NULL; + + /* all output devices are prefixed with this */ + device_id = g_string_new ("xrandr"); + + /* get the output EDID if possible */ + edid = gcm_session_get_output_edid (state, output, &error); + if (edid == NULL) { + g_debug ("no edid for %s [%s], falling back to connection name", + gnome_rr_output_get_name (output), + error->message); + g_error_free (error); + g_string_append_printf (device_id, + "-%s", + gnome_rr_output_get_name (output)); + goto out; + } + + /* check EDID data is okay to use */ + vendor = gcm_edid_get_vendor_name (edid); + name = gcm_edid_get_monitor_name (edid); + serial = gcm_edid_get_serial_number (edid); + if (vendor == NULL && name == NULL && serial == NULL) { + g_debug ("edid invalid for %s, falling back to connection name", + gnome_rr_output_get_name (output)); + g_string_append_printf (device_id, + "-%s", + gnome_rr_output_get_name (output)); + goto out; + } + + /* use EDID data */ + if (vendor != NULL) + g_string_append_printf (device_id, "-%s", vendor); + if (name != NULL) + g_string_append_printf (device_id, "-%s", name); + if (serial != NULL) + g_string_append_printf (device_id, "-%s", serial); +out: + if (edid != NULL) + g_object_unref (edid); + return g_string_free (device_id, FALSE); +} + +typedef struct { + GsdColorState *state; + CdProfile *profile; + CdDevice *device; + guint32 output_id; +} GcmSessionAsyncHelper; + +static void +gcm_session_async_helper_free (GcmSessionAsyncHelper *helper) +{ + if (helper->state != NULL) + g_object_unref (helper->state); + if (helper->profile != NULL) + g_object_unref (helper->profile); + if (helper->device != NULL) + g_object_unref (helper->device); + g_free (helper); +} + +static gboolean +gcm_utils_mkdir_for_filename (GFile *file, GError **error) +{ + gboolean ret = FALSE; + GFile *parent_dir = NULL; + + /* get parent directory */ + parent_dir = g_file_get_parent (file); + if (parent_dir == NULL) { + g_set_error_literal (error, + GSD_COLOR_MANAGER_ERROR, + GSD_COLOR_MANAGER_ERROR_FAILED, + "could not get parent dir"); + goto out; + } + + /* ensure desination does not already exist */ + ret = g_file_query_exists (parent_dir, NULL); + if (ret) + goto out; + ret = g_file_make_directory_with_parents (parent_dir, NULL, error); + if (!ret) + goto out; +out: + if (parent_dir != NULL) + g_object_unref (parent_dir); + return ret; +} + +static gboolean +gcm_get_system_icc_profile (GsdColorState *state, + GFile *file) +{ + const char efi_path[] = "/sys/firmware/efi/efivars/INTERNAL_PANEL_COLOR_INFO-01e1ada1-79f2-46b3-8d3e-71fc0996ca6b"; + /* efi variables have a 4-byte header */ + const int efi_var_header_length = 4; + g_autoptr(GFile) efi_file = g_file_new_for_path (efi_path); + gboolean ret; + g_autofree char *data = NULL; + gsize length; + g_autoptr(GError) error = NULL; + + ret = g_file_query_exists (efi_file, NULL); + if (!ret) + return FALSE; + + ret = g_file_load_contents (efi_file, + NULL /* cancellable */, + &data, + &length, + NULL /* etag_out */, + &error); + + if (!ret) { + g_warning ("failed to read EFI system color profile: %s", + error->message); + return FALSE; + } + + if (length <= efi_var_header_length) { + g_warning ("EFI system color profile was too short"); + return FALSE; + } + + ret = g_file_replace_contents (file, + data + efi_var_header_length, + length - efi_var_header_length, + NULL /* etag */, + FALSE /* make_backup */, + G_FILE_CREATE_NONE, + NULL /* new_etag */, + NULL /* cancellable */, + &error); + if (!ret) { + g_warning ("failed to write system color profile: %s", + error->message); + return FALSE; + } + + return TRUE; +} + +static gboolean +gcm_apply_create_icc_profile_for_edid (GsdColorState *state, + CdDevice *device, + GcmEdid *edid, + GFile *file, + GError **error) +{ + CdIcc *icc = NULL; + const gchar *data; + gboolean ret = FALSE; + + /* ensure the per-user directory exists */ + ret = gcm_utils_mkdir_for_filename (file, error); + if (!ret) + goto out; + + /* create our generated profile */ + icc = cd_icc_new (); + ret = cd_icc_create_from_edid (icc, + gcm_edid_get_gamma (edid), + gcm_edid_get_red (edid), + gcm_edid_get_green (edid), + gcm_edid_get_blue (edid), + gcm_edid_get_white (edid), + error); + if (!ret) + goto out; + + /* set copyright */ + cd_icc_set_copyright (icc, NULL, + /* deliberately not translated */ + "This profile is free of known copyright restrictions."); + + /* set model and title */ + data = gcm_edid_get_monitor_name (edid); + if (data == NULL) + data = cd_client_get_system_model (state->client); + if (data == NULL) + data = "Unknown monitor"; + cd_icc_set_model (icc, NULL, data); + cd_icc_set_description (icc, NULL, data); + + /* get manufacturer */ + data = gcm_edid_get_vendor_name (edid); + if (data == NULL) + data = cd_client_get_system_vendor (state->client); + if (data == NULL) + data = "Unknown vendor"; + cd_icc_set_manufacturer (icc, NULL, data); + + /* set the framework creator metadata */ + cd_icc_add_metadata (icc, + CD_PROFILE_METADATA_CMF_PRODUCT, + PACKAGE_NAME); + cd_icc_add_metadata (icc, + CD_PROFILE_METADATA_CMF_BINARY, + PACKAGE_NAME); + cd_icc_add_metadata (icc, + CD_PROFILE_METADATA_CMF_VERSION, + PACKAGE_VERSION); + cd_icc_add_metadata (icc, + CD_PROFILE_METADATA_MAPPING_DEVICE_ID, + cd_device_get_id (device)); + + /* set 'ICC meta Tag for Monitor Profiles' data */ + cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MD5, gcm_edid_get_checksum (edid)); + data = gcm_edid_get_monitor_name (edid); + if (data != NULL) + cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MODEL, data); + data = gcm_edid_get_serial_number (edid); + if (data != NULL) + cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_SERIAL, data); + data = gcm_edid_get_pnp_id (edid); + if (data != NULL) + cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MNFT, data); + data = gcm_edid_get_vendor_name (edid); + if (data != NULL) + cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_VENDOR, data); + + /* save */ + ret = cd_icc_save_file (icc, file, CD_ICC_SAVE_FLAGS_NONE, NULL, error); + if (!ret) + goto out; +out: + if (icc != NULL) + g_object_unref (icc); + return ret; +} + +static GPtrArray * +gcm_session_generate_vcgt (CdProfile *profile, guint color_temperature, guint size) +{ + GnomeRROutputClutItem *tmp; + GPtrArray *array = NULL; + const cmsToneCurve **vcgt; + cmsFloat32Number in; + guint i; + cmsHPROFILE lcms_profile; + CdIcc *icc = NULL; + CdColorRGB temp; + + /* invalid size */ + if (size == 0) + goto out; + + /* open file */ + icc = cd_profile_load_icc (profile, CD_ICC_LOAD_FLAGS_NONE, NULL, NULL); + if (icc == NULL) + goto out; + + /* get tone curves from profile */ + lcms_profile = cd_icc_get_handle (icc); + vcgt = cmsReadTag (lcms_profile, cmsSigVcgtTag); + if (vcgt == NULL || vcgt[0] == NULL) { + g_debug ("profile does not have any VCGT data"); + goto out; + } + + /* get the color temperature */ + if (!cd_color_get_blackbody_rgb_full (color_temperature, + &temp, + CD_COLOR_BLACKBODY_FLAG_USE_PLANCKIAN)) { + g_warning ("failed to get blackbody for %uK", color_temperature); + cd_color_rgb_set (&temp, 1.0, 1.0, 1.0); + } else { + g_debug ("using VCGT gamma of %uK = %.1f,%.1f,%.1f", + color_temperature, temp.R, temp.G, temp.B); + } + + /* create array */ + array = g_ptr_array_new_with_free_func (g_free); + for (i = 0; i < size; i++) { + in = (gdouble) i / (gdouble) (size - 1); + tmp = g_new0 (GnomeRROutputClutItem, 1); + tmp->red = cmsEvalToneCurveFloat(vcgt[0], in) * temp.R * (gdouble) 0xffff; + tmp->green = cmsEvalToneCurveFloat(vcgt[1], in) * temp.G * (gdouble) 0xffff; + tmp->blue = cmsEvalToneCurveFloat(vcgt[2], in) * temp.B * (gdouble) 0xffff; + g_ptr_array_add (array, tmp); + } +out: + if (icc != NULL) + g_object_unref (icc); + return array; +} + +static guint +gnome_rr_output_get_gamma_size (GnomeRROutput *output) +{ + GnomeRRCrtc *crtc; + gint len = 0; + + crtc = gnome_rr_output_get_crtc (output); + if (crtc == NULL) + return 0; + gnome_rr_crtc_get_gamma (crtc, + &len, + NULL, NULL, NULL); + return (guint) len; +} + +static gboolean +gcm_session_output_set_gamma (GnomeRROutput *output, + GPtrArray *array, + GError **error) +{ + gboolean ret = TRUE; + guint16 *red = NULL; + guint16 *green = NULL; + guint16 *blue = NULL; + guint i; + GnomeRROutputClutItem *data; + GnomeRRCrtc *crtc; + + /* no length? */ + if (array->len == 0) { + ret = FALSE; + g_set_error_literal (error, + GSD_COLOR_MANAGER_ERROR, + GSD_COLOR_MANAGER_ERROR_FAILED, + "no data in the CLUT array"); + goto out; + } + + /* convert to a type X understands */ + red = g_new (guint16, array->len); + green = g_new (guint16, array->len); + blue = g_new (guint16, array->len); + for (i = 0; i < array->len; i++) { + data = g_ptr_array_index (array, i); + red[i] = data->red; + green[i] = data->green; + blue[i] = data->blue; + } + + /* send to LUT */ + crtc = gnome_rr_output_get_crtc (output); + if (crtc == NULL) { + ret = FALSE; + g_set_error (error, + GSD_COLOR_MANAGER_ERROR, + GSD_COLOR_MANAGER_ERROR_FAILED, + "failed to get ctrc for %s", + gnome_rr_output_get_name (output)); + goto out; + } + gnome_rr_crtc_set_gamma (crtc, array->len, + red, green, blue); +out: + g_free (red); + g_free (green); + g_free (blue); + return ret; +} + +static gboolean +gcm_session_device_set_gamma (GnomeRROutput *output, + CdProfile *profile, + guint color_temperature, + GError **error) +{ + gboolean ret = FALSE; + guint size; + GPtrArray *clut = NULL; + + /* create a lookup table */ + size = gnome_rr_output_get_gamma_size (output); + if (size == 0) { + ret = TRUE; + goto out; + } + clut = gcm_session_generate_vcgt (profile, color_temperature, size); + if (clut == NULL) { + g_set_error_literal (error, + GSD_COLOR_MANAGER_ERROR, + GSD_COLOR_MANAGER_ERROR_FAILED, + "failed to generate vcgt"); + goto out; + } + + /* apply the vcgt to this output */ + ret = gcm_session_output_set_gamma (output, clut, error); + if (!ret) + goto out; +out: + if (clut != NULL) + g_ptr_array_unref (clut); + return ret; +} + +static gboolean +gcm_session_device_reset_gamma (GnomeRROutput *output, + guint color_temperature, + GError **error) +{ + gboolean ret; + guint i; + guint size; + guint32 value; + GPtrArray *clut; + GnomeRROutputClutItem *data; + CdColorRGB temp; + + /* create a linear ramp */ + g_debug ("falling back to dummy ramp"); + clut = g_ptr_array_new_with_free_func (g_free); + size = gnome_rr_output_get_gamma_size (output); + if (size == 0) { + ret = TRUE; + goto out; + } + + /* get the color temperature */ + if (!cd_color_get_blackbody_rgb_full (color_temperature, + &temp, + CD_COLOR_BLACKBODY_FLAG_USE_PLANCKIAN)) { + g_warning ("failed to get blackbody for %uK", color_temperature); + cd_color_rgb_set (&temp, 1.0, 1.0, 1.0); + } else { + g_debug ("using reset gamma of %uK = %.1f,%.1f,%.1f", + color_temperature, temp.R, temp.G, temp.B); + } + + for (i = 0; i < size; i++) { + value = (i * 0xffff) / (size - 1); + data = g_new0 (GnomeRROutputClutItem, 1); + data->red = value * temp.R; + data->green = value * temp.G; + data->blue = value * temp.B; + g_ptr_array_add (clut, data); + } + + /* apply the vcgt to this output */ + ret = gcm_session_output_set_gamma (output, clut, error); + if (!ret) + goto out; +out: + g_ptr_array_unref (clut); + return ret; +} + +static GnomeRROutput * +gcm_session_get_state_output_by_id (GsdColorState *state, + const gchar *device_id, + GError **error) +{ + gchar *output_id; + GnomeRROutput *output = NULL; + GnomeRROutput **outputs = NULL; + guint i; + + /* search all STATE outputs for the device id */ + outputs = gnome_rr_screen_list_outputs (state->state_screen); + if (outputs == NULL) { + g_set_error_literal (error, + GSD_COLOR_MANAGER_ERROR, + GSD_COLOR_MANAGER_ERROR_FAILED, + "Failed to get outputs"); + goto out; + } + for (i = 0; outputs[i] != NULL && output == NULL; i++) { + output_id = gcm_session_get_output_id (state, outputs[i]); + if (g_strcmp0 (output_id, device_id) == 0) + output = outputs[i]; + g_free (output_id); + } + if (output == NULL) { + g_set_error (error, + GSD_COLOR_MANAGER_ERROR, + GSD_COLOR_MANAGER_ERROR_FAILED, + "Failed to find output %s", + device_id); + } +out: + return output; +} + +/* this function is more complicated than it should be, due to the + * fact that XOrg sometimes assigns no primary devices when using + * "xrandr --auto" or when the version of RANDR is < 1.3 */ +static gboolean +gcm_session_use_output_profile_for_screen (GsdColorState *state, + GnomeRROutput *output) +{ + gboolean has_laptop = FALSE; + gboolean has_primary = FALSE; + GnomeRROutput **outputs; + GnomeRROutput *connected = NULL; + guint i; + + /* do we have any screens marked as primary */ + outputs = gnome_rr_screen_list_outputs (state->state_screen); + if (outputs == NULL || outputs[0] == NULL) { + g_warning ("failed to get outputs"); + return FALSE; + } + for (i = 0; outputs[i] != NULL; i++) { + if (connected == NULL) + connected = outputs[i]; + if (gnome_rr_output_get_is_primary (outputs[i])) + has_primary = TRUE; + if (gnome_rr_output_is_builtin_display (outputs[i])) + has_laptop = TRUE; + } + + /* we have an assigned primary device, are we that? */ + if (has_primary) + return gnome_rr_output_get_is_primary (output); + + /* choosing the internal panel is probably sane */ + if (has_laptop) + return gnome_rr_output_is_builtin_display (output); + + /* we have to choose one, so go for the first connected device */ + if (connected != NULL) + return gnome_rr_output_get_id (connected) == gnome_rr_output_get_id (output); + + return FALSE; +} + +/* TODO: remove when we can dep on a released version of colord */ +#ifndef CD_PROFILE_METADATA_SCREEN_BRIGHTNESS +#define CD_PROFILE_METADATA_SCREEN_BRIGHTNESS "SCREEN_brightness" +#endif + +#define GSD_DBUS_NAME_POWER GSD_DBUS_NAME ".Power" +#define GSD_DBUS_INTERFACE_POWER_SCREEN GSD_DBUS_BASE_INTERFACE ".Power.Screen" +#define GSD_DBUS_PATH_POWER GSD_DBUS_PATH "/Power" + +static void +gcm_session_set_output_percentage (guint percentage) +{ + GDBusConnection *connection; + + /* get a ref to the existing bus connection */ + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + if (connection == NULL) + return; + g_dbus_connection_call (connection, + GSD_DBUS_NAME_POWER, + GSD_DBUS_PATH_POWER, + "org.freedesktop.DBus.Properties", + "Set", + g_variant_new_parsed ("('" GSD_DBUS_INTERFACE_POWER_SCREEN "'," + "'Brightness', %v)", + g_variant_new_int32 (percentage)), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL, NULL); + g_object_unref (connection); +} + +static void +gcm_session_device_assign_profile_connect_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + CdProfile *profile = CD_PROFILE (object); + const gchar *brightness_profile; + const gchar *filename; + gboolean ret; + GError *error = NULL; + GnomeRROutput *output; + guint brightness_percentage; + GcmSessionAsyncHelper *helper = (GcmSessionAsyncHelper *) user_data; + GsdColorState *state = GSD_COLOR_STATE (helper->state); + + /* get properties */ + ret = cd_profile_connect_finish (profile, res, &error); + if (!ret) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("failed to connect to profile: %s", error->message); + g_error_free (error); + goto out; + } + + /* get the filename */ + filename = cd_profile_get_filename (profile); + g_assert (filename != NULL); + + /* get the output (can't save in helper as GnomeRROutput isn't + * a GObject, just a pointer */ + output = gnome_rr_screen_get_output_by_id (state->state_screen, + helper->output_id); + if (output == NULL) + goto out; + + /* if output is a laptop screen and the profile has a + * calibration brightness then set this new brightness */ + brightness_profile = cd_profile_get_metadata_item (profile, + CD_PROFILE_METADATA_SCREEN_BRIGHTNESS); + if (gnome_rr_output_is_builtin_display (output) && + brightness_profile != NULL) { + /* the percentage is stored in the profile metadata as + * a string, not ideal, but it's all we have... */ + brightness_percentage = atoi (brightness_profile); + gcm_session_set_output_percentage (brightness_percentage); + } + + /* set the _ICC_PROFILE atom */ + ret = gcm_session_use_output_profile_for_screen (state, output); + if (ret) { + ret = gcm_session_screen_set_icc_profile (state, + filename, + &error); + if (!ret) { + g_warning ("failed to set screen _ICC_PROFILE: %s", + error->message); + g_clear_error (&error); + } + } + + /* create a vcgt for this icc file */ + ret = cd_profile_get_has_vcgt (profile); + if (ret) { + ret = gcm_session_device_set_gamma (output, + profile, + state->color_temperature, + &error); + if (!ret) { + g_warning ("failed to set %s gamma tables: %s", + cd_device_get_id (helper->device), + error->message); + g_error_free (error); + goto out; + } + } else { + ret = gcm_session_device_reset_gamma (output, + state->color_temperature, + &error); + if (!ret) { + g_warning ("failed to reset %s gamma tables: %s", + cd_device_get_id (helper->device), + error->message); + g_error_free (error); + goto out; + } + } +out: + gcm_session_async_helper_free (helper); +} + +/* + * Check to see if the on-disk profile has the MAPPING_device_id + * metadata, and if not, we should delete the profile and re-create it + * so that it gets mapped by the daemon. + */ +static gboolean +gcm_session_check_profile_device_md (GFile *file) +{ + const gchar *key_we_need = CD_PROFILE_METADATA_MAPPING_DEVICE_ID; + CdIcc *icc; + gboolean ret; + + icc = cd_icc_new (); + ret = cd_icc_load_file (icc, file, CD_ICC_LOAD_FLAGS_METADATA, NULL, NULL); + if (!ret) + goto out; + ret = cd_icc_get_metadata_item (icc, key_we_need) != NULL; + if (!ret) { + g_debug ("auto-edid profile is old, and contains no %s data", + key_we_need); + } +out: + g_object_unref (icc); + return ret; +} + +static void +gcm_session_device_assign_connect_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + CdDeviceKind kind; + CdProfile *profile = NULL; + gboolean ret; + gchar *autogen_filename = NULL; + gchar *autogen_path = NULL; + GcmEdid *edid = NULL; + GnomeRROutput *output = NULL; + GError *error = NULL; + GFile *file = NULL; + const gchar *xrandr_id; + GcmSessionAsyncHelper *helper; + CdDevice *device = CD_DEVICE (object); + GsdColorState *state = GSD_COLOR_STATE (user_data); + + /* remove from assign array */ + g_hash_table_remove (state->device_assign_hash, + cd_device_get_object_path (device)); + + /* get properties */ + ret = cd_device_connect_finish (device, res, &error); + if (!ret) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("failed to connect to device: %s", error->message); + g_error_free (error); + goto out; + } + + /* check we care */ + kind = cd_device_get_kind (device); + if (kind != CD_DEVICE_KIND_DISPLAY) + goto out; + + g_debug ("need to assign display device %s", + cd_device_get_id (device)); + + /* get the GnomeRROutput for the device id */ + xrandr_id = cd_device_get_id (device); + output = gcm_session_get_state_output_by_id (state, + xrandr_id, + &error); + if (output == NULL) { + g_warning ("no %s device found: %s", + cd_device_get_id (device), + error->message); + g_error_free (error); + goto out; + } + + /* create profile from device edid if it exists */ + edid = gcm_session_get_output_edid (state, output, &error); + if (edid == NULL) { + g_warning ("unable to get EDID for %s: %s", + cd_device_get_id (device), + error->message); + g_clear_error (&error); + + } else { + autogen_filename = g_strdup_printf ("edid-%s.icc", + gcm_edid_get_checksum (edid)); + autogen_path = g_build_filename (g_get_user_data_dir (), + "icc", autogen_filename, NULL); + + /* check if auto-profile has up-to-date metadata */ + file = g_file_new_for_path (autogen_path); + if (gcm_session_check_profile_device_md (file)) { + g_debug ("auto-profile edid %s exists with md", autogen_path); + } else { + g_debug ("auto-profile edid does not exist, creating as %s", + autogen_path); + + /* check if the system has a built-in profile */ + ret = gnome_rr_output_is_builtin_display (output) && + gcm_get_system_icc_profile (state, file); + + /* try creating one from the EDID */ + if (!ret) { + ret = gcm_apply_create_icc_profile_for_edid (state, + device, + edid, + file, + &error); + } + + if (!ret) { + g_warning ("failed to create profile from EDID data: %s", + error->message); + g_clear_error (&error); + } + } + } + + /* get the default profile for the device */ + profile = cd_device_get_default_profile (device); + if (profile == NULL) { + g_debug ("%s has no default profile to set", + cd_device_get_id (device)); + + /* the default output? */ + if (gnome_rr_output_get_is_primary (output) && + state->gdk_window != NULL) { + gdk_property_delete (state->gdk_window, + gdk_atom_intern_static_string ("_ICC_PROFILE")); + gdk_property_delete (state->gdk_window, + gdk_atom_intern_static_string ("_ICC_PROFILE_IN_X_VERSION")); + } + + /* reset, as we want linear profiles for profiling */ + ret = gcm_session_device_reset_gamma (output, + state->color_temperature, + &error); + if (!ret) { + g_warning ("failed to reset %s gamma tables: %s", + cd_device_get_id (device), + error->message); + g_error_free (error); + goto out; + } + goto out; + } + + /* get properties */ + helper = g_new0 (GcmSessionAsyncHelper, 1); + helper->output_id = gnome_rr_output_get_id (output); + helper->state = g_object_ref (state); + helper->device = g_object_ref (device); + cd_profile_connect (profile, + state->cancellable, + gcm_session_device_assign_profile_connect_cb, + helper); +out: + g_free (autogen_filename); + g_free (autogen_path); + if (file != NULL) + g_object_unref (file); + if (edid != NULL) + g_object_unref (edid); + if (profile != NULL) + g_object_unref (profile); +} + +static void +gcm_session_device_assign (GsdColorState *state, CdDevice *device) +{ + const gchar *key; + gpointer found; + + /* are we already assigning this device */ + key = cd_device_get_object_path (device); + found = g_hash_table_lookup (state->device_assign_hash, key); + if (found != NULL) { + g_debug ("assign for %s already in progress", key); + return; + } + g_hash_table_insert (state->device_assign_hash, + g_strdup (key), + GINT_TO_POINTER (TRUE)); + cd_device_connect (device, + state->cancellable, + gcm_session_device_assign_connect_cb, + state); +} + +static void +gcm_session_device_added_assign_cb (CdClient *client, + CdDevice *device, + GsdColorState *state) +{ + gcm_session_device_assign (state, device); +} + +static void +gcm_session_device_changed_assign_cb (CdClient *client, + CdDevice *device, + GsdColorState *state) +{ + g_debug ("%s changed", cd_device_get_object_path (device)); + gcm_session_device_assign (state, device); +} + +static void +gcm_session_create_device_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + CdDevice *device; + GError *error = NULL; + + device = cd_client_create_device_finish (CD_CLIENT (object), + res, + &error); + if (device == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, CD_CLIENT_ERROR, CD_CLIENT_ERROR_ALREADY_EXISTS)) + g_warning ("failed to create device: %s", error->message); + g_error_free (error); + return; + } + g_object_unref (device); +} + +static void +gcm_session_add_state_output (GsdColorState *state, GnomeRROutput *output) +{ + const gchar *edid_checksum = NULL; + const gchar *model = NULL; + const gchar *output_name = NULL; + const gchar *serial = NULL; + const gchar *vendor = NULL; + gboolean ret; + gchar *device_id = NULL; + GcmEdid *edid; + GError *error = NULL; + GHashTable *device_props = NULL; + + /* VNC creates a fake device that cannot be color managed */ + output_name = gnome_rr_output_get_name (output); + if (output_name != NULL && g_str_has_prefix (output_name, "VNC-")) { + g_debug ("ignoring %s as fake VNC device detected", output_name); + return; + } + + /* try to get edid */ + edid = gcm_session_get_output_edid (state, output, &error); + if (edid == NULL) { + g_warning ("failed to get edid: %s", + error->message); + g_clear_error (&error); + } + + /* prefer DMI data for the internal output */ + ret = gnome_rr_output_is_builtin_display (output); + if (ret) { + model = cd_client_get_system_model (state->client); + vendor = cd_client_get_system_vendor (state->client); + } + + /* use EDID data if we have it */ + if (edid != NULL) { + edid_checksum = gcm_edid_get_checksum (edid); + if (model == NULL) + model = gcm_edid_get_monitor_name (edid); + if (vendor == NULL) + vendor = gcm_edid_get_vendor_name (edid); + if (serial == NULL) + serial = gcm_edid_get_serial_number (edid); + } + + /* ensure mandatory fields are set */ + if (model == NULL) + model = gnome_rr_output_get_name (output); + if (vendor == NULL) + vendor = "unknown"; + if (serial == NULL) + serial = "unknown"; + + device_id = gcm_session_get_output_id (state, output); + g_debug ("output %s added", device_id); + device_props = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, NULL); + g_hash_table_insert (device_props, + (gpointer) CD_DEVICE_PROPERTY_KIND, + (gpointer) cd_device_kind_to_string (CD_DEVICE_KIND_DISPLAY)); + g_hash_table_insert (device_props, + (gpointer) CD_DEVICE_PROPERTY_MODE, + (gpointer) cd_device_mode_to_string (CD_DEVICE_MODE_PHYSICAL)); + g_hash_table_insert (device_props, + (gpointer) CD_DEVICE_PROPERTY_COLORSPACE, + (gpointer) cd_colorspace_to_string (CD_COLORSPACE_RGB)); + g_hash_table_insert (device_props, + (gpointer) CD_DEVICE_PROPERTY_VENDOR, + (gpointer) vendor); + g_hash_table_insert (device_props, + (gpointer) CD_DEVICE_PROPERTY_MODEL, + (gpointer) model); + g_hash_table_insert (device_props, + (gpointer) CD_DEVICE_PROPERTY_SERIAL, + (gpointer) serial); + g_hash_table_insert (device_props, + (gpointer) CD_DEVICE_METADATA_XRANDR_NAME, + (gpointer) gnome_rr_output_get_name (output)); + g_hash_table_insert (device_props, + (gpointer) CD_DEVICE_METADATA_OUTPUT_PRIORITY, + gnome_rr_output_get_is_primary (output) ? + (gpointer) CD_DEVICE_METADATA_OUTPUT_PRIORITY_PRIMARY : + (gpointer) CD_DEVICE_METADATA_OUTPUT_PRIORITY_SECONDARY); + if (edid_checksum != NULL) { + g_hash_table_insert (device_props, + (gpointer) CD_DEVICE_METADATA_OUTPUT_EDID_MD5, + (gpointer) edid_checksum); + } + /* set this so we can call the device a 'Laptop Screen' in the + * control center main panel */ + if (gnome_rr_output_is_builtin_display (output)) { + g_hash_table_insert (device_props, + (gpointer) CD_DEVICE_PROPERTY_EMBEDDED, + NULL); + } + cd_client_create_device (state->client, + device_id, + CD_OBJECT_SCOPE_TEMP, + device_props, + state->cancellable, + gcm_session_create_device_cb, + state); + g_free (device_id); + if (device_props != NULL) + g_hash_table_unref (device_props); + if (edid != NULL) + g_object_unref (edid); +} + + +static void +gnome_rr_screen_output_added_cb (GnomeRRScreen *screen, + GnomeRROutput *output, + GsdColorState *state) +{ + gcm_session_add_state_output (state, output); +} + +static void +gcm_session_screen_removed_delete_device_cb (GObject *object, GAsyncResult *res, gpointer user_data) +{ + gboolean ret; + GError *error = NULL; + + /* deleted device */ + ret = cd_client_delete_device_finish (CD_CLIENT (object), + res, + &error); + if (!ret) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("failed to delete device: %s", error->message); + g_error_free (error); + } +} + +static void +gcm_session_screen_removed_find_device_cb (GObject *object, GAsyncResult *res, gpointer user_data) +{ + GError *error = NULL; + CdDevice *device; + GsdColorState *state = GSD_COLOR_STATE (user_data); + + device = cd_client_find_device_finish (state->client, + res, + &error); + if (device == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("failed to find device: %s", error->message); + g_error_free (error); + return; + } + g_debug ("output %s found, and will be removed", + cd_device_get_object_path (device)); + cd_client_delete_device (state->client, + device, + state->cancellable, + gcm_session_screen_removed_delete_device_cb, + state); + g_object_unref (device); +} + +static void +gnome_rr_screen_output_removed_cb (GnomeRRScreen *screen, + GnomeRROutput *output, + GsdColorState *state) +{ + g_debug ("output %s removed", + gnome_rr_output_get_name (output)); + g_hash_table_remove (state->edid_cache, + gnome_rr_output_get_name (output)); + cd_client_find_device_by_property (state->client, + CD_DEVICE_METADATA_XRANDR_NAME, + gnome_rr_output_get_name (output), + state->cancellable, + gcm_session_screen_removed_find_device_cb, + state); +} + +static void +gcm_session_get_devices_cb (GObject *object, GAsyncResult *res, gpointer user_data) +{ + CdDevice *device; + GError *error = NULL; + GPtrArray *array; + guint i; + GsdColorState *state = GSD_COLOR_STATE (user_data); + + array = cd_client_get_devices_finish (CD_CLIENT (object), res, &error); + if (array == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("failed to get devices: %s", error->message); + g_error_free (error); + return; + } + for (i = 0; i < array->len; i++) { + device = g_ptr_array_index (array, i); + gcm_session_device_assign (state, device); + } + + if (array != NULL) + g_ptr_array_unref (array); +} + +static void +gcm_session_profile_gamma_find_device_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + CdClient *client = CD_CLIENT (object); + CdDevice *device = NULL; + GError *error = NULL; + GsdColorState *state = GSD_COLOR_STATE (user_data); + + device = cd_client_find_device_by_property_finish (client, + res, + &error); + if (device == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("could not find device: %s", error->message); + g_error_free (error); + return; + } + + /* get properties */ + cd_device_connect (device, + state->cancellable, + gcm_session_device_assign_connect_cb, + state); + + if (device != NULL) + g_object_unref (device); +} + +static void +gcm_session_set_gamma_for_all_devices (GsdColorState *state) +{ + GnomeRROutput **outputs; + guint i; + + /* setting the temperature before we get the list of devices is fine, + * as we use the temperature in the calculation */ + if (state->state_screen == NULL) + return; + + /* get STATE outputs */ + outputs = gnome_rr_screen_list_outputs (state->state_screen); + if (outputs == NULL) { + g_warning ("failed to get outputs"); + return; + } + for (i = 0; outputs[i] != NULL; i++) { + /* get CdDevice for this output */ + cd_client_find_device_by_property (state->client, + CD_DEVICE_METADATA_XRANDR_NAME, + gnome_rr_output_get_name (outputs[i]), + state->cancellable, + gcm_session_profile_gamma_find_device_cb, + state); + } +} + +/* We have to reset the gamma tables each time as if the primary output + * has changed then different crtcs are going to be used. + * See https://bugzilla.gnome.org/show_bug.cgi?id=660164 for an example */ +static void +gnome_rr_screen_output_changed_cb (GnomeRRScreen *screen, + GsdColorState *state) +{ + gcm_session_set_gamma_for_all_devices (state); +} + +static gboolean +has_changed (char **strv, + const char *str) +{ + guint i; + for (i = 0; strv[i] != NULL; i++) { + if (g_str_equal (str, strv[i])) + return TRUE; + } + return FALSE; +} + +static void +gcm_session_active_changed_cb (GDBusProxy *session, + GVariant *changed, + char **invalidated, + GsdColorState *state) +{ + GVariant *active_v = NULL; + gboolean is_active; + + if (has_changed (invalidated, "SessionIsActive")) + return; + + /* not yet connected to the daemon */ + if (!cd_client_get_connected (state->client)) + return; + + active_v = g_dbus_proxy_get_cached_property (session, "SessionIsActive"); + g_return_if_fail (active_v != NULL); + is_active = g_variant_get_boolean (active_v); + g_variant_unref (active_v); + + /* When doing the fast-user-switch into a new account, load the + * new users chosen profiles. + * + * If this is the first time the GnomeSettingsSession has been + * loaded, then we'll get a change from unknown to active + * and we want to avoid reprobing the devices for that. + */ + if (is_active && !state->session_is_active) { + g_debug ("Done switch to new account, reload devices"); + cd_client_get_devices (state->client, + state->cancellable, + gcm_session_get_devices_cb, + state); + } + state->session_is_active = is_active; +} + +static void +gcm_session_client_connect_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + gboolean ret; + GError *error = NULL; + GnomeRROutput **outputs; + guint i; + GsdColorState *state = GSD_COLOR_STATE (user_data); + + /* connected */ + ret = cd_client_connect_finish (state->client, res, &error); + if (!ret) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("failed to connect to colord: %s", error->message); + g_error_free (error); + return; + } + + /* is there an available colord instance? */ + ret = cd_client_get_has_server (state->client); + if (!ret) { + g_warning ("There is no colord server available"); + return; + } + + /* watch if sessions change */ + g_signal_connect_object (state->session, "g-properties-changed", + G_CALLBACK (gcm_session_active_changed_cb), + state, 0); + + /* add screens */ + gnome_rr_screen_refresh (state->state_screen, &error); + if (error != NULL) { + g_warning ("failed to refresh: %s", error->message); + g_error_free (error); + return; + } + + /* get STATE outputs */ + outputs = gnome_rr_screen_list_outputs (state->state_screen); + if (outputs == NULL) { + g_warning ("failed to get outputs"); + return; + } + for (i = 0; outputs[i] != NULL; i++) { + gcm_session_add_state_output (state, outputs[i]); + } + + /* only connect when colord is awake */ + g_signal_connect (state->state_screen, "output-connected", + G_CALLBACK (gnome_rr_screen_output_added_cb), + state); + g_signal_connect (state->state_screen, "output-disconnected", + G_CALLBACK (gnome_rr_screen_output_removed_cb), + state); + g_signal_connect (state->state_screen, "changed", + G_CALLBACK (gnome_rr_screen_output_changed_cb), + state); + + g_signal_connect (state->client, "device-added", + G_CALLBACK (gcm_session_device_added_assign_cb), + state); + g_signal_connect (state->client, "device-changed", + G_CALLBACK (gcm_session_device_changed_assign_cb), + state); + + /* set for each device that already exist */ + cd_client_get_devices (state->client, + state->cancellable, + gcm_session_get_devices_cb, + state); +} + +static void +on_rr_screen_acquired (GObject *object, + GAsyncResult *result, + gpointer data) +{ + GsdColorState *state = data; + GnomeRRScreen *screen; + GError *error = NULL; + + /* gnome_rr_screen_new_async() does not take a GCancellable */ + if (g_cancellable_is_cancelled (state->cancellable)) + goto out; + + screen = gnome_rr_screen_new_finish (result, &error); + if (screen == NULL) { + g_warning ("failed to get screens: %s", error->message); + g_error_free (error); + goto out; + } + + state->state_screen = screen; + + cd_client_connect (state->client, + state->cancellable, + gcm_session_client_connect_cb, + state); +out: + /* manually added */ + g_object_unref (state); +} + +void +gsd_color_state_start (GsdColorState *state) +{ + /* use a fresh cancellable for each start->stop operation */ + g_cancellable_cancel (state->cancellable); + g_clear_object (&state->cancellable); + state->cancellable = g_cancellable_new (); + + /* coldplug the list of screens */ + gnome_rr_screen_new_async (gdk_screen_get_default (), + on_rr_screen_acquired, + g_object_ref (state)); +} + +void +gsd_color_state_stop (GsdColorState *state) +{ + g_cancellable_cancel (state->cancellable); +} + +static void +gsd_color_state_class_init (GsdColorStateClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_color_state_finalize; +} + +static void +gsd_color_state_init (GsdColorState *state) +{ + /* track the active session */ + state->session = gnome_settings_bus_get_session_proxy (); + +#ifdef GDK_WINDOWING_X11 + /* set the _ICC_PROFILE atoms on the root screen */ + if (GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + state->gdk_window = gdk_screen_get_root_window (gdk_screen_get_default ()); +#endif + + /* parsing the EDID is expensive */ + state->edid_cache = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_object_unref); + + /* we don't want to assign devices multiple times at startup */ + state->device_assign_hash = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + NULL); + + /* default color temperature */ + state->color_temperature = GSD_COLOR_TEMPERATURE_DEFAULT; + + state->client = cd_client_new (); +} + +static void +gsd_color_state_finalize (GObject *object) +{ + GsdColorState *state; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_COLOR_STATE (object)); + + state = GSD_COLOR_STATE (object); + + g_cancellable_cancel (state->cancellable); + g_clear_object (&state->cancellable); + g_clear_object (&state->client); + g_clear_object (&state->session); + g_clear_pointer (&state->edid_cache, g_hash_table_destroy); + g_clear_pointer (&state->device_assign_hash, g_hash_table_destroy); + g_clear_object (&state->state_screen); + + G_OBJECT_CLASS (gsd_color_state_parent_class)->finalize (object); +} + +GsdColorState * +gsd_color_state_new (void) +{ + GsdColorState *state; + state = g_object_new (GSD_TYPE_COLOR_STATE, NULL); + return GSD_COLOR_STATE (state); +} diff --git a/plugins/color/gsd-color-state.h b/plugins/color/gsd-color-state.h new file mode 100644 index 0000000..f1c8827 --- /dev/null +++ b/plugins/color/gsd-color-state.h @@ -0,0 +1,47 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_COLOR_STATE_H +#define __GSD_COLOR_STATE_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_COLOR_STATE (gsd_color_state_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdColorState, gsd_color_state, GSD, COLOR_STATE, GObject) + +#define GSD_COLOR_TEMPERATURE_MIN 1000 /* Kelvin */ +#define GSD_COLOR_TEMPERATURE_DEFAULT 6500 /* Kelvin, is RGB [1.0,1.0,1.0] */ +#define GSD_COLOR_TEMPERATURE_MAX 10000 /* Kelvin */ + +GQuark gsd_color_state_error_quark (void); + +GsdColorState * gsd_color_state_new (void); +void gsd_color_state_start (GsdColorState *state); +void gsd_color_state_stop (GsdColorState *state); +void gsd_color_state_set_temperature (GsdColorState *state, + guint temperature); +guint gsd_color_state_get_temperature (GsdColorState *state); + +G_END_DECLS + +#endif /* __GSD_COLOR_STATE_H */ diff --git a/plugins/color/gsd-night-light-common.c b/plugins/color/gsd-night-light-common.c new file mode 100644 index 0000000..5fe756e --- /dev/null +++ b/plugins/color/gsd-night-light-common.c @@ -0,0 +1,137 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <glib.h> +#include <math.h> + +#include "gsd-night-light-common.h" + +static gdouble +deg2rad (gdouble degrees) +{ + return (M_PI * degrees) / 180.f; +} + +static gdouble +rad2deg (gdouble radians) +{ + return radians * (180.f / M_PI); +} + +/* + * Formulas taken from https://www.esrl.noaa.gov/gmd/grad/solcalc/calcdetails.html + * + * The returned values are fractional hours, so 6am would be 6.0 and 4:30pm + * would be 16.5. + * + * The values returned by this function might not make sense for locations near + * the polar regions. For example, in the north of Lapland there might not be + * a sunrise at all. + */ +gboolean +gsd_night_light_get_sunrise_sunset (GDateTime *dt, + gdouble pos_lat, gdouble pos_long, + gdouble *sunrise, gdouble *sunset) +{ + g_autoptr(GDateTime) dt_zero = g_date_time_new_utc (1900, 1, 1, 0, 0, 0); + GTimeSpan ts = g_date_time_difference (dt, dt_zero); + + g_return_val_if_fail (pos_lat <= 90.f && pos_lat >= -90.f, FALSE); + g_return_val_if_fail (pos_long <= 180.f && pos_long >= -180.f, FALSE); + + gdouble tz_offset = (gdouble) g_date_time_get_utc_offset (dt) / G_USEC_PER_SEC / 60 / 60; // B5 + gdouble date_as_number = ts / G_USEC_PER_SEC / 24 / 60 / 60 + 2; // B7 + gdouble time_past_local_midnight = 0; // E2, unused in this calculation + gdouble julian_day = date_as_number + 2415018.5 + + time_past_local_midnight - tz_offset / 24; + gdouble julian_century = (julian_day - 2451545) / 36525; + gdouble geom_mean_long_sun = fmod (280.46646 + julian_century * + (36000.76983 + julian_century * 0.0003032), 360); // I2 + gdouble geom_mean_anom_sun = 357.52911 + julian_century * + (35999.05029 - 0.0001537 * julian_century); // J2 + gdouble eccent_earth_orbit = 0.016708634 - julian_century * + (0.000042037 + 0.0000001267 * julian_century); // K2 + gdouble sun_eq_of_ctr = sin (deg2rad (geom_mean_anom_sun)) * + (1.914602 - julian_century * (0.004817 + 0.000014 * julian_century)) + + sin (deg2rad (2 * geom_mean_anom_sun)) * (0.019993 - 0.000101 * julian_century) + + sin (deg2rad (3 * geom_mean_anom_sun)) * 0.000289; // L2 + gdouble sun_true_long = geom_mean_long_sun + sun_eq_of_ctr; // M2 + gdouble sun_app_long = sun_true_long - 0.00569 - 0.00478 * + sin (deg2rad (125.04 - 1934.136 * julian_century)); // P2 + gdouble mean_obliq_ecliptic = 23 + (26 + ((21.448 - julian_century * + (46.815 + julian_century * (0.00059 - julian_century * 0.001813)))) / 60) / 60; // Q2 + gdouble obliq_corr = mean_obliq_ecliptic + 0.00256 * + cos (deg2rad (125.04 - 1934.136 * julian_century)); // R2 + gdouble sun_declin = rad2deg (asin (sin (deg2rad (obliq_corr)) * + sin (deg2rad (sun_app_long)))); // T2 + gdouble var_y = tan (deg2rad (obliq_corr/2)) * tan (deg2rad (obliq_corr / 2)); // U2 + gdouble eq_of_time = 4 * rad2deg (var_y * sin (2 * deg2rad (geom_mean_long_sun)) - + 2 * eccent_earth_orbit * sin (deg2rad (geom_mean_anom_sun)) + + 4 * eccent_earth_orbit * var_y * + sin (deg2rad (geom_mean_anom_sun)) * + cos (2 * deg2rad (geom_mean_long_sun)) - + 0.5 * var_y * var_y * sin (4 * deg2rad (geom_mean_long_sun)) - + 1.25 * eccent_earth_orbit * eccent_earth_orbit * + sin (2 * deg2rad (geom_mean_anom_sun))); // V2 + gdouble ha_sunrise = rad2deg (acos (cos (deg2rad (90.833)) / (cos (deg2rad (pos_lat)) * + cos (deg2rad (sun_declin))) - tan (deg2rad (pos_lat)) * + tan (deg2rad (sun_declin)))); // W2 + gdouble solar_noon = (720 - 4 * pos_long - eq_of_time + tz_offset * 60) / 1440; // X2 + gdouble sunrise_time = solar_noon - ha_sunrise * 4 / 1440; // Y2 + gdouble sunset_time = solar_noon + ha_sunrise * 4 / 1440; // Z2 + + /* convert to hours */ + if (sunrise != NULL) + *sunrise = sunrise_time * 24; + if (sunset != NULL) + *sunset = sunset_time * 24; + return TRUE; +} + +gdouble +gsd_night_light_frac_day_from_dt (GDateTime *dt) +{ + return g_date_time_get_hour (dt) + + (gdouble) g_date_time_get_minute (dt) / 60.f + + (gdouble) g_date_time_get_second (dt) / 3600.f; +} + +gboolean +gsd_night_light_frac_day_is_between (gdouble value, + gdouble start, + gdouble end) +{ + /* wrap end to the next day if it is before start, + * considering equal values as a full 24h period + */ + if (end <= start) + end += 24; + + /* wrap value to the next day if it is before the range */ + if (value < start && value < end) + value += 24; + + /* Check whether value falls into range; together with the 24h + * wrap around above this means that TRUE is always returned when + * start == end. + */ + return value >= start && value < end; +} diff --git a/plugins/color/gsd-night-light-common.h b/plugins/color/gsd-night-light-common.h new file mode 100644 index 0000000..4995da5 --- /dev/null +++ b/plugins/color/gsd-night-light-common.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_NIGHT_LIGHT_COMMON_H +#define __GSD_NIGHT_LIGHT_COMMON_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +gboolean gsd_night_light_get_sunrise_sunset (GDateTime *dt, + gdouble pos_lat, + gdouble pos_long, + gdouble *sunrise, + gdouble *sunset); +gdouble gsd_night_light_frac_day_from_dt (GDateTime *dt); +gboolean gsd_night_light_frac_day_is_between (gdouble value, + gdouble start, + gdouble end); + +G_END_DECLS + +#endif /* __GSD_NIGHT_LIGHT_COMMON_H */ diff --git a/plugins/color/gsd-night-light.c b/plugins/color/gsd-night-light.c new file mode 100644 index 0000000..635f160 --- /dev/null +++ b/plugins/color/gsd-night-light.c @@ -0,0 +1,801 @@ +/* + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <geoclue.h> + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include "gnome-datetime-source.h" + +#include "gsd-color-state.h" + +#include "gsd-night-light.h" +#include "gsd-night-light-common.h" + +struct _GsdNightLight { + GObject parent; + GSettings *settings; + gboolean forced; + gboolean disabled_until_tmw; + GDateTime *disabled_until_tmw_dt; + gboolean geoclue_enabled; + GSource *source; + guint validate_id; + GClueClient *geoclue_client; + GClueSimple *geoclue_simple; + GSettings *location_settings; + gdouble cached_sunrise; + gdouble cached_sunset; + gdouble cached_temperature; + gboolean cached_active; + gboolean smooth_enabled; + GTimer *smooth_timer; + guint smooth_id; + gdouble smooth_target_temperature; + GCancellable *cancellable; + GDateTime *datetime_override; +}; + +enum { + PROP_0, + PROP_ACTIVE, + PROP_SUNRISE, + PROP_SUNSET, + PROP_TEMPERATURE, + PROP_DISABLED_UNTIL_TMW, + PROP_FORCED, + PROP_LAST +}; + +#define GSD_NIGHT_LIGHT_SCHEDULE_TIMEOUT 5 /* seconds */ +#define GSD_NIGHT_LIGHT_POLL_TIMEOUT 60 /* seconds */ +#define GSD_NIGHT_LIGHT_POLL_SMEAR 1 /* hours */ +#define GSD_NIGHT_LIGHT_SMOOTH_SMEAR 5.f /* seconds */ + +#define GSD_FRAC_DAY_MAX_DELTA (1.f/60.f) /* 1 minute */ +#define GSD_TEMPERATURE_MAX_DELTA (10.f) /* Kelvin */ + +#define DESKTOP_ID "gnome-color-panel" + +static void poll_timeout_destroy (GsdNightLight *self); +static void poll_timeout_create (GsdNightLight *self); +static void night_light_recheck (GsdNightLight *self); + +G_DEFINE_TYPE (GsdNightLight, gsd_night_light, G_TYPE_OBJECT); + +static GDateTime * +gsd_night_light_get_date_time_now (GsdNightLight *self) +{ + if (self->datetime_override != NULL) + return g_date_time_ref (self->datetime_override); + return g_date_time_new_now_local (); +} + +void +gsd_night_light_set_date_time_now (GsdNightLight *self, GDateTime *datetime) +{ + if (self->datetime_override != NULL) + g_date_time_unref (self->datetime_override); + self->datetime_override = g_date_time_ref (datetime); + + night_light_recheck (self); +} + +static void +poll_smooth_destroy (GsdNightLight *self) +{ + if (self->smooth_id != 0) { + g_source_remove (self->smooth_id); + self->smooth_id = 0; + } + if (self->smooth_timer != NULL) + g_clear_pointer (&self->smooth_timer, g_timer_destroy); +} + +void +gsd_night_light_set_smooth_enabled (GsdNightLight *self, + gboolean smooth_enabled) +{ + /* ensure the timeout is stopped if called at runtime */ + if (!smooth_enabled) + poll_smooth_destroy (self); + self->smooth_enabled = smooth_enabled; +} + +static gdouble +linear_interpolate (gdouble val1, gdouble val2, gdouble factor) +{ + g_return_val_if_fail (factor >= 0.f, -1.f); + g_return_val_if_fail (factor <= 1.f, -1.f); + return ((val1 - val2) * factor) + val2; +} + +static gboolean +update_cached_sunrise_sunset (GsdNightLight *self) +{ + gboolean ret = FALSE; + gdouble latitude; + gdouble longitude; + gdouble sunrise; + gdouble sunset; + g_autoptr(GVariant) tmp = NULL; + g_autoptr(GDateTime) dt_now = gsd_night_light_get_date_time_now (self); + + /* calculate the sunrise/sunset for the location */ + tmp = g_settings_get_value (self->settings, "night-light-last-coordinates"); + g_variant_get (tmp, "(dd)", &latitude, &longitude); + if (latitude > 90.f || latitude < -90.f) + return FALSE; + if (longitude > 180.f || longitude < -180.f) + return FALSE; + if (!gsd_night_light_get_sunrise_sunset (dt_now, latitude, longitude, + &sunrise, &sunset)) { + g_warning ("failed to get sunset/sunrise for %.3f,%.3f", + longitude, longitude); + return FALSE; + } + + /* anything changed */ + if (ABS (self->cached_sunrise - sunrise) > GSD_FRAC_DAY_MAX_DELTA) { + self->cached_sunrise = sunrise; + g_object_notify (G_OBJECT (self), "sunrise"); + ret = TRUE; + } + if (ABS (self->cached_sunset - sunset) > GSD_FRAC_DAY_MAX_DELTA) { + self->cached_sunset = sunset; + g_object_notify (G_OBJECT (self), "sunset"); + ret = TRUE; + } + return ret; +} + +static void +gsd_night_light_set_temperature_internal (GsdNightLight *self, gdouble temperature) +{ + if (ABS (self->cached_temperature - temperature) <= GSD_TEMPERATURE_MAX_DELTA) + return; + self->cached_temperature = temperature; + g_object_notify (G_OBJECT (self), "temperature"); +} + +static gboolean +gsd_night_light_smooth_cb (gpointer user_data) +{ + GsdNightLight *self = GSD_NIGHT_LIGHT (user_data); + gdouble tmp; + gdouble frac; + + /* find fraction */ + frac = g_timer_elapsed (self->smooth_timer, NULL) / GSD_NIGHT_LIGHT_SMOOTH_SMEAR; + if (frac >= 1.f) { + gsd_night_light_set_temperature_internal (self, + self->smooth_target_temperature); + self->smooth_id = 0; + return G_SOURCE_REMOVE; + } + + /* set new temperature step using log curve */ + tmp = self->smooth_target_temperature - self->cached_temperature; + tmp *= frac; + tmp += self->cached_temperature; + gsd_night_light_set_temperature_internal (self, tmp); + + return G_SOURCE_CONTINUE; +} + +static void +poll_smooth_create (GsdNightLight *self, gdouble temperature) +{ + g_assert (self->smooth_id == 0); + self->smooth_target_temperature = temperature; + self->smooth_timer = g_timer_new (); + self->smooth_id = g_timeout_add (50, gsd_night_light_smooth_cb, self); +} + +static void +gsd_night_light_set_temperature (GsdNightLight *self, gdouble temperature) +{ + /* immediate */ + if (!self->smooth_enabled) { + gsd_night_light_set_temperature_internal (self, temperature); + return; + } + + /* Destroy any smooth transition, it will be recreated if neccessary */ + poll_smooth_destroy (self); + + /* small jump */ + if (ABS (temperature - self->cached_temperature) < GSD_TEMPERATURE_MAX_DELTA) { + gsd_night_light_set_temperature_internal (self, temperature); + return; + } + + /* smooth out the transition */ + poll_smooth_create (self, temperature); +} + +static void +gsd_night_light_set_active (GsdNightLight *self, gboolean active) +{ + if (self->cached_active == active) + return; + self->cached_active = active; + + /* ensure set to unity temperature */ + if (!active) + gsd_night_light_set_temperature (self, GSD_COLOR_TEMPERATURE_DEFAULT); + + g_object_notify (G_OBJECT (self), "active"); +} + +static void +night_light_recheck (GsdNightLight *self) +{ + gdouble frac_day; + gdouble schedule_from = -1.f; + gdouble schedule_to = -1.f; + gdouble smear = GSD_NIGHT_LIGHT_POLL_SMEAR; /* hours */ + guint temperature; + guint temp_smeared; + g_autoptr(GDateTime) dt_now = gsd_night_light_get_date_time_now (self); + + /* Forced mode, just set the temperature to night light. + * Proper rechecking will happen once forced mode is disabled again */ + if (self->forced) { + temperature = g_settings_get_uint (self->settings, "night-light-temperature"); + gsd_night_light_set_temperature (self, temperature); + return; + } + + /* enabled */ + if (!g_settings_get_boolean (self->settings, "night-light-enabled")) { + g_debug ("night light disabled, resetting"); + gsd_night_light_set_active (self, FALSE); + return; + } + + /* calculate the position of the sun */ + if (g_settings_get_boolean (self->settings, "night-light-schedule-automatic")) { + update_cached_sunrise_sunset (self); + if (self->cached_sunrise > 0.f && self->cached_sunset > 0.f) { + schedule_to = self->cached_sunrise; + schedule_from = self->cached_sunset; + } + } + + /* fall back to manual settings */ + if (schedule_to <= 0.f || schedule_from <= 0.f) { + schedule_from = g_settings_get_double (self->settings, + "night-light-schedule-from"); + schedule_to = g_settings_get_double (self->settings, + "night-light-schedule-to"); + } + + /* get the current hour of a day as a fraction */ + frac_day = gsd_night_light_frac_day_from_dt (dt_now); + g_debug ("fractional day = %.3f, limits = %.3f->%.3f", + frac_day, schedule_from, schedule_to); + + /* disabled until tomorrow */ + if (self->disabled_until_tmw) { + GTimeSpan time_span; + gboolean reset = FALSE; + + time_span = g_date_time_difference (dt_now, self->disabled_until_tmw_dt); + + /* Reset if disabled until tomorrow is more than 24h ago. */ + if (time_span > (GTimeSpan) 24 * 60 * 60 * 1000000) { + g_debug ("night light disabled until tomorrow is older than 24h, resetting disabled until tomorrow"); + reset = TRUE; + } else if (time_span > 0) { + /* Or if a sunrise lies between the time it was disabled and now. */ + gdouble frac_disabled; + frac_disabled = gsd_night_light_frac_day_from_dt (self->disabled_until_tmw_dt); + if (frac_disabled != frac_day && + gsd_night_light_frac_day_is_between (schedule_to, + frac_disabled, + frac_day)) { + g_debug ("night light sun rise happened, resetting disabled until tomorrow"); + reset = TRUE; + } + } + + if (reset) { + self->disabled_until_tmw = FALSE; + g_clear_pointer(&self->disabled_until_tmw_dt, g_date_time_unref); + g_object_notify (G_OBJECT (self), "disabled-until-tmw"); + } else { + g_debug ("night light still day-disabled, resetting"); + gsd_night_light_set_temperature (self, + GSD_COLOR_TEMPERATURE_DEFAULT); + return; + } + } + + /* lower smearing period to be smaller than the time between start/stop */ + smear = MIN (smear, + MIN ( ABS (schedule_to - schedule_from), + 24 - ABS (schedule_to - schedule_from))); + + if (!gsd_night_light_frac_day_is_between (frac_day, + schedule_from - smear, + schedule_to)) { + g_debug ("not time for night-light"); + gsd_night_light_set_active (self, FALSE); + return; + } + + /* smear the temperature for a short duration before the set limits + * + * |----------------------| = from->to + * |-| = smear down + * |-| = smear up + * + * \ / + * \ / + * \--------------------/ + */ + temperature = g_settings_get_uint (self->settings, "night-light-temperature"); + if (smear < 0.01) { + /* Don't try to smear for extremely short or zero periods */ + temp_smeared = temperature; + } else if (gsd_night_light_frac_day_is_between (frac_day, + schedule_from - smear, + schedule_from)) { + gdouble factor = 1.f - ((frac_day - (schedule_from - smear)) / smear); + temp_smeared = linear_interpolate (GSD_COLOR_TEMPERATURE_DEFAULT, + temperature, factor); + } else if (gsd_night_light_frac_day_is_between (frac_day, + schedule_to - smear, + schedule_to)) { + gdouble factor = (frac_day - (schedule_to - smear)) / smear; + temp_smeared = linear_interpolate (GSD_COLOR_TEMPERATURE_DEFAULT, + temperature, factor); + } else { + temp_smeared = temperature; + } + g_debug ("night light mode on, using temperature of %uK (aiming for %uK)", + temp_smeared, temperature); + gsd_night_light_set_active (self, TRUE); + gsd_night_light_set_temperature (self, temp_smeared); +} + +static gboolean +night_light_recheck_schedule_cb (gpointer user_data) +{ + GsdNightLight *self = GSD_NIGHT_LIGHT (user_data); + night_light_recheck (self); + self->validate_id = 0; + return G_SOURCE_REMOVE; +} + +/* called when something changed */ +static void +night_light_recheck_schedule (GsdNightLight *self) +{ + if (self->validate_id != 0) + g_source_remove (self->validate_id); + self->validate_id = + g_timeout_add_seconds (GSD_NIGHT_LIGHT_SCHEDULE_TIMEOUT, + night_light_recheck_schedule_cb, + self); +} + +/* called when the time may have changed */ +static gboolean +night_light_recheck_cb (gpointer user_data) +{ + GsdNightLight *self = GSD_NIGHT_LIGHT (user_data); + + /* recheck parameters, then reschedule a new timeout */ + night_light_recheck (self); + poll_timeout_destroy (self); + poll_timeout_create (self); + + /* return value ignored for a one-time watch */ + return G_SOURCE_REMOVE; +} + +static void +poll_timeout_create (GsdNightLight *self) +{ + g_autoptr(GDateTime) dt_now = NULL; + g_autoptr(GDateTime) dt_expiry = NULL; + + if (self->source != NULL) + return; + + /* It is not a good idea to make this overridable, it just creates + * an infinite loop as a fixed date for testing just doesn't work. */ + dt_now = g_date_time_new_now_local (); + dt_expiry = g_date_time_add_seconds (dt_now, GSD_NIGHT_LIGHT_POLL_TIMEOUT); + self->source = _gnome_datetime_source_new (dt_now, + dt_expiry, + TRUE); + g_source_set_callback (self->source, + night_light_recheck_cb, + self, NULL); + g_source_attach (self->source, NULL); +} + +static void +poll_timeout_destroy (GsdNightLight *self) +{ + + if (self->source == NULL) + return; + + g_source_destroy (self->source); + g_source_unref (self->source); + self->source = NULL; +} + +static void +settings_changed_cb (GSettings *settings, gchar *key, gpointer user_data) +{ + GsdNightLight *self = GSD_NIGHT_LIGHT (user_data); + g_debug ("settings changed"); + night_light_recheck (self); +} + +static void +on_location_notify (GClueSimple *simple, + GParamSpec *pspec, + gpointer user_data) +{ + GsdNightLight *self = GSD_NIGHT_LIGHT (user_data); + GClueLocation *location; + gdouble latitude, longitude; + + location = gclue_simple_get_location (simple); + latitude = gclue_location_get_latitude (location); + longitude = gclue_location_get_longitude (location); + + g_settings_set_value (self->settings, + "night-light-last-coordinates", + g_variant_new ("(dd)", latitude, longitude)); + + g_debug ("got geoclue latitude %f, longitude %f", latitude, longitude); + + /* recheck the levels if the location changed significantly */ + if (update_cached_sunrise_sunset (self)) + night_light_recheck_schedule (self); +} + +static void +on_geoclue_simple_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsdNightLight *self = GSD_NIGHT_LIGHT (user_data); + GClueSimple *geoclue_simple; + g_autoptr(GError) error = NULL; + + geoclue_simple = gclue_simple_new_finish (res, &error); + if (geoclue_simple == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to connect to GeoClue2 service: %s", error->message); + return; + } + + self->geoclue_simple = geoclue_simple; + self->geoclue_client = gclue_simple_get_client (self->geoclue_simple); + g_object_set (G_OBJECT (self->geoclue_client), + "time-threshold", 60*60, NULL); /* 1 hour */ + + g_signal_connect (self->geoclue_simple, "notify::location", + G_CALLBACK (on_location_notify), user_data); + + on_location_notify (self->geoclue_simple, NULL, user_data); +} + +static void +start_geoclue (GsdNightLight *self) +{ + self->cancellable = g_cancellable_new (); + gclue_simple_new (DESKTOP_ID, + GCLUE_ACCURACY_LEVEL_CITY, + self->cancellable, + on_geoclue_simple_ready, + self); + +} + +static void +stop_geoclue (GsdNightLight *self) +{ + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + if (self->geoclue_client != NULL) { + gclue_client_call_stop (self->geoclue_client, NULL, NULL, NULL); + self->geoclue_client = NULL; + } + g_clear_object (&self->geoclue_simple); +} + +static void +check_location_settings (GsdNightLight *self) +{ + if (g_settings_get_boolean (self->location_settings, "enabled") && self->geoclue_enabled) + start_geoclue (self); + else + stop_geoclue (self); +} + +void +gsd_night_light_set_disabled_until_tmw (GsdNightLight *self, gboolean value) +{ + g_autoptr(GDateTime) dt = gsd_night_light_get_date_time_now (self); + + if (self->disabled_until_tmw == value) + return; + + self->disabled_until_tmw = value; + g_clear_pointer (&self->disabled_until_tmw_dt, g_date_time_unref); + if (self->disabled_until_tmw) + self->disabled_until_tmw_dt = g_steal_pointer (&dt); + night_light_recheck (self); + g_object_notify (G_OBJECT (self), "disabled-until-tmw"); +} + +gboolean +gsd_night_light_get_disabled_until_tmw (GsdNightLight *self) +{ + return self->disabled_until_tmw; +} + +void +gsd_night_light_set_forced (GsdNightLight *self, gboolean value) +{ + if (self->forced == value) + return; + + self->forced = value; + g_object_notify (G_OBJECT (self), "forced"); + + /* A simple recheck might not reset the temperature if + * night light is currently disabled. */ + if (!self->forced && !self->cached_active) + gsd_night_light_set_temperature (self, GSD_COLOR_TEMPERATURE_DEFAULT); + + night_light_recheck (self); +} + +gboolean +gsd_night_light_get_forced (GsdNightLight *self) +{ + return self->forced; +} + +gboolean +gsd_night_light_get_active (GsdNightLight *self) +{ + return self->cached_active; +} + +gdouble +gsd_night_light_get_sunrise (GsdNightLight *self) +{ + return self->cached_sunrise; +} + +gdouble +gsd_night_light_get_sunset (GsdNightLight *self) +{ + return self->cached_sunset; +} + +gdouble +gsd_night_light_get_temperature (GsdNightLight *self) +{ + return self->cached_temperature; +} + +void +gsd_night_light_set_geoclue_enabled (GsdNightLight *self, gboolean enabled) +{ + self->geoclue_enabled = enabled; +} + +gboolean +gsd_night_light_start (GsdNightLight *self, GError **error) +{ + night_light_recheck (self); + poll_timeout_create (self); + + /* care about changes */ + g_signal_connect (self->settings, "changed", + G_CALLBACK (settings_changed_cb), self); + + g_signal_connect_swapped (self->location_settings, "changed::enabled", + G_CALLBACK (check_location_settings), self); + check_location_settings (self); + + return TRUE; +} + +static void +gsd_night_light_finalize (GObject *object) +{ + GsdNightLight *self = GSD_NIGHT_LIGHT (object); + + stop_geoclue (self); + + poll_timeout_destroy (self); + poll_smooth_destroy (self); + + g_clear_object (&self->settings); + g_clear_pointer (&self->datetime_override, g_date_time_unref); + g_clear_pointer (&self->disabled_until_tmw_dt, g_date_time_unref); + + if (self->validate_id > 0) { + g_source_remove (self->validate_id); + self->validate_id = 0; + } + + g_clear_object (&self->location_settings); + G_OBJECT_CLASS (gsd_night_light_parent_class)->finalize (object); +} + +static void +gsd_night_light_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsdNightLight *self = GSD_NIGHT_LIGHT (object); + + switch (prop_id) { + case PROP_SUNRISE: + self->cached_sunrise = g_value_get_double (value); + break; + case PROP_SUNSET: + self->cached_sunset = g_value_get_double (value); + break; + case PROP_TEMPERATURE: + self->cached_temperature = g_value_get_double (value); + break; + case PROP_DISABLED_UNTIL_TMW: + gsd_night_light_set_disabled_until_tmw (self, g_value_get_boolean (value)); + break; + case PROP_FORCED: + gsd_night_light_set_forced (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gsd_night_light_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsdNightLight *self = GSD_NIGHT_LIGHT (object); + + switch (prop_id) { + case PROP_ACTIVE: + g_value_set_boolean (value, self->cached_active); + break; + case PROP_SUNRISE: + g_value_set_double (value, self->cached_sunrise); + break; + case PROP_SUNSET: + g_value_set_double (value, self->cached_sunrise); + break; + case PROP_TEMPERATURE: + g_value_set_double (value, self->cached_sunrise); + break; + case PROP_DISABLED_UNTIL_TMW: + g_value_set_boolean (value, gsd_night_light_get_disabled_until_tmw (self)); + break; + case PROP_FORCED: + g_value_set_boolean (value, gsd_night_light_get_forced (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gsd_night_light_class_init (GsdNightLightClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gsd_night_light_finalize; + + object_class->set_property = gsd_night_light_set_property; + object_class->get_property = gsd_night_light_get_property; + + g_object_class_install_property (object_class, + PROP_ACTIVE, + g_param_spec_boolean ("active", + "Active", + "If night light functionality is active right now", + FALSE, + G_PARAM_READABLE)); + + g_object_class_install_property (object_class, + PROP_SUNRISE, + g_param_spec_double ("sunrise", + "Sunrise", + "Sunrise in fractional hours", + 0, + 24.f, + 12, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_SUNSET, + g_param_spec_double ("sunset", + "Sunset", + "Sunset in fractional hours", + 0, + 24.f, + 12, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_TEMPERATURE, + g_param_spec_double ("temperature", + "Temperature", + "Temperature in Kelvin", + GSD_COLOR_TEMPERATURE_MIN, + GSD_COLOR_TEMPERATURE_MAX, + GSD_COLOR_TEMPERATURE_DEFAULT, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_DISABLED_UNTIL_TMW, + g_param_spec_boolean ("disabled-until-tmw", + "Disabled until tomorrow", + "If the night light is disabled until the next day", + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_FORCED, + g_param_spec_boolean ("forced", + "Forced", + "Whether night light should be forced on, useful for previewing", + FALSE, + G_PARAM_READWRITE)); + +} + +static void +gsd_night_light_init (GsdNightLight *self) +{ + self->geoclue_enabled = TRUE; + self->smooth_enabled = TRUE; + self->cached_sunrise = -1.f; + self->cached_sunset = -1.f; + self->cached_temperature = GSD_COLOR_TEMPERATURE_DEFAULT; + self->settings = g_settings_new ("org.gnome.settings-daemon.plugins.color"); + self->location_settings = g_settings_new ("org.gnome.system.location"); +} + +GsdNightLight * +gsd_night_light_new (void) +{ + return g_object_new (GSD_TYPE_NIGHT_LIGHT, NULL); +} diff --git a/plugins/color/gsd-night-light.h b/plugins/color/gsd-night-light.h new file mode 100644 index 0000000..8803c33 --- /dev/null +++ b/plugins/color/gsd-night-light.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __GSD_NIGHT_LIGHT_H__ +#define __GSD_NIGHT_LIGHT_H__ + +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_NIGHT_LIGHT (gsd_night_light_get_type ()) +G_DECLARE_FINAL_TYPE (GsdNightLight, gsd_night_light, GSD, NIGHT_LIGHT, GObject) + +GsdNightLight *gsd_night_light_new (void); +gboolean gsd_night_light_start (GsdNightLight *self, + GError **error); + +gboolean gsd_night_light_get_active (GsdNightLight *self); +gdouble gsd_night_light_get_sunrise (GsdNightLight *self); +gdouble gsd_night_light_get_sunset (GsdNightLight *self); +gdouble gsd_night_light_get_temperature (GsdNightLight *self); + +gboolean gsd_night_light_get_disabled_until_tmw (GsdNightLight *self); +void gsd_night_light_set_disabled_until_tmw (GsdNightLight *self, + gboolean value); + +gboolean gsd_night_light_get_forced (GsdNightLight *self); +void gsd_night_light_set_forced (GsdNightLight *self, + gboolean value); + +/* only for the self test program */ +void gsd_night_light_set_geoclue_enabled (GsdNightLight *self, + gboolean enabled); +void gsd_night_light_set_date_time_now (GsdNightLight *self, + GDateTime *datetime); +void gsd_night_light_set_smooth_enabled (GsdNightLight *self, + gboolean smooth_enabled); + +G_END_DECLS + +#endif diff --git a/plugins/color/main.c b/plugins/color/main.c new file mode 100644 index 0000000..5dda3e7 --- /dev/null +++ b/plugins/color/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_color_manager_new +#define START gsd_color_manager_start +#define STOP gsd_color_manager_stop +#define MANAGER GsdColorManager +#include "gsd-color-manager.h" + +#include "daemon-skeleton-gtk.h" diff --git a/plugins/color/meson.build b/plugins/color/meson.build new file mode 100644 index 0000000..2ae1740 --- /dev/null +++ b/plugins/color/meson.build @@ -0,0 +1,55 @@ +sources = files( + 'gcm-edid.c', + 'gnome-datetime-source.c', + 'gsd-color-calibrate.c', + 'gsd-color-manager.c', + 'gsd-color-profiles.c', + 'gsd-color-state.c', + 'gsd-night-light.c', + 'gsd-night-light-common.c', + 'main.c' +) + +deps = plugins_deps + [ + colord_dep, + gnome_desktop_dep, + lcms_dep, + libcanberra_gtk_dep, + libgeoclue_dep, + libnotify_dep, + m_dep, +] + +cflags += ['-DBINDIR="@0@"'.format(gsd_bindir)] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) + +sources = files( + 'gcm-edid.c', + 'gcm-self-test.c', + 'gnome-datetime-source.c', + 'gsd-night-light.c', + 'gsd-night-light-common.c' +) + +test_unit = 'gcm-self-test' + +exe = executable( + test_unit, + sources, + include_directories: top_inc, + dependencies: deps, + c_args: '-DTESTDATADIR="@0@"'.format(join_paths(meson.current_source_dir(), 'test-data')) +) + +envs = ['GSETTINGS_SCHEMA_DIR=@0@'.format(join_paths(meson.build_root(), 'data'))] +test(test_unit, exe, env: envs) diff --git a/plugins/color/test-data/LG-L225W-External.bin b/plugins/color/test-data/LG-L225W-External.bin Binary files differnew file mode 100644 index 0000000..f08310a --- /dev/null +++ b/plugins/color/test-data/LG-L225W-External.bin diff --git a/plugins/color/test-data/Lenovo-T61-Internal.bin b/plugins/color/test-data/Lenovo-T61-Internal.bin Binary files differnew file mode 100644 index 0000000..45aec9d --- /dev/null +++ b/plugins/color/test-data/Lenovo-T61-Internal.bin diff --git a/plugins/common/daemon-skeleton-gtk.h b/plugins/common/daemon-skeleton-gtk.h new file mode 100644 index 0000000..596281d --- /dev/null +++ b/plugins/common/daemon-skeleton-gtk.h @@ -0,0 +1,292 @@ +/** + * Create a gnome-settings-daemon helper easily + * + * #define NEW gsd_media_keys_manager_new + * #define START gsd_media_keys_manager_start + * #define MANAGER GsdMediaKeysManager + * #include "gsd-media-keys-manager.h" + * + * #include "daemon-skeleton-gtk.h" + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <locale.h> + +#include <glib-unix.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include "gnome-settings-bus.h" + +#ifndef PLUGIN_NAME +#error Include PLUGIN_CFLAGS in the daemon s CFLAGS +#endif /* !PLUGIN_NAME */ + +#define GNOME_SESSION_DBUS_NAME "org.gnome.SessionManager" +#define GNOME_SESSION_CLIENT_PRIVATE_DBUS_INTERFACE "org.gnome.SessionManager.ClientPrivate" + +static MANAGER *manager = NULL; +static int timeout = -1; +static char *dummy_name = NULL; +static gboolean verbose = FALSE; + +static GOptionEntry entries[] = { + { "exit-time", 0, 0, G_OPTION_ARG_INT, &timeout, "Exit after n seconds time", NULL }, + { "dummy-name", 0, 0, G_OPTION_ARG_STRING, &dummy_name, "Name when using the dummy daemon", NULL }, + { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "Verbose", NULL }, + {NULL} +}; + +static void +respond_to_end_session (GDBusProxy *proxy) +{ + /* we must answer with "EndSessionResponse" */ + g_dbus_proxy_call (proxy, "EndSessionResponse", + g_variant_new ("(bs)", TRUE, ""), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL, NULL); +} + +static void +do_stop (void) +{ + gtk_main_quit (); +} + +static void +client_proxy_signal_cb (GDBusProxy *proxy, + gchar *sender_name, + gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + if (g_strcmp0 (signal_name, "QueryEndSession") == 0) { + g_debug ("Got QueryEndSession signal"); + respond_to_end_session (proxy); + } else if (g_strcmp0 (signal_name, "EndSession") == 0) { + g_debug ("Got EndSession signal"); + respond_to_end_session (proxy); + } else if (g_strcmp0 (signal_name, "Stop") == 0) { + g_debug ("Got Stop signal"); + do_stop (); + } +} + +static void +on_client_registered (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GVariant *variant; + GDBusProxy *client_proxy; + GError *error = NULL; + gchar *object_path = NULL; + + variant = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), res, &error); + if (!variant) { + g_warning ("Unable to register client: %s", error->message); + g_error_free (error); + return; + } + + g_variant_get (variant, "(o)", &object_path); + + g_debug ("Registered client at path %s", object_path); + + client_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, 0, NULL, + GNOME_SESSION_DBUS_NAME, + object_path, + GNOME_SESSION_CLIENT_PRIVATE_DBUS_INTERFACE, + NULL, + &error); + if (!client_proxy) { + g_warning ("Unable to get the session client proxy: %s", error->message); + g_error_free (error); + return; + } + + g_signal_connect (client_proxy, "g-signal", + G_CALLBACK (client_proxy_signal_cb), NULL); + + g_free (object_path); + g_variant_unref (variant); +} + +static void +register_with_gnome_session (void) +{ + GDBusProxy *proxy; + const char *startup_id; + + proxy = G_DBUS_PROXY (gnome_settings_bus_get_session_proxy ()); + startup_id = g_getenv ("DESKTOP_AUTOSTART_ID"); + g_dbus_proxy_call (proxy, + "RegisterClient", + g_variant_new ("(ss)", dummy_name ? dummy_name : PLUGIN_NAME, startup_id ? startup_id : ""), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + (GAsyncReadyCallback) on_client_registered, + NULL); + + /* DESKTOP_AUTOSTART_ID must not leak into child processes, because + * it can't be reused. Child processes will not know whether this is + * a genuine value or erroneous already-used value. */ + g_unsetenv ("DESKTOP_AUTOSTART_ID"); +} + +static void +set_empty_gtk_theme (gboolean set) +{ + static char *old_gtk_theme = NULL; + + if (set) { + /* Override GTK_THEME to reduce overhead of CSS engine. By using + * GTK_THEME environment variable, GtkSettings is not allowed to + * initially parse the Adwaita theme. + * + * https://bugzilla.gnome.org/show_bug.cgi?id=780555 */ + old_gtk_theme = g_strdup (g_getenv ("GTK_THEME")); + g_setenv ("GTK_THEME", "Disabled", TRUE); + } else { + /* GtkSettings has loaded, so we can drop GTK_THEME used to initialize + * our internal theme. Only the main thread accesses the GTK_THEME + * environment variable, so this is safe to release. */ + if (old_gtk_theme != NULL) + g_setenv ("GTK_THEME", old_gtk_theme, TRUE); + else + g_unsetenv ("GTK_THEME"); + } +} + +static gboolean +handle_sigterm (gpointer user_data) +{ + g_debug ("Got SIGTERM; shutting down ..."); + + if (gtk_main_level () > 0) + gtk_main_quit (); + + return G_SOURCE_REMOVE; +} + +static void +install_signal_handler (void) +{ + g_autoptr(GSource) source = NULL; + + source = g_unix_signal_source_new (SIGTERM); + + g_source_set_callback (source, handle_sigterm, NULL, NULL); + g_source_attach (source, NULL); +} + +static void +bus_acquired_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data G_GNUC_UNUSED) +{ + g_debug ("%s: acquired bus %p for name %s", G_STRFUNC, connection, name); +} + +static void +name_acquired_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data G_GNUC_UNUSED) +{ + g_debug ("%s: acquired name %s on bus %p", G_STRFUNC, name, connection); +} + +static void +name_lost_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data G_GNUC_UNUSED) +{ + g_debug ("%s: lost name %s on bus %p", G_STRFUNC, name, connection); +} + +int +main (int argc, char **argv) +{ + GError *error = NULL; + guint name_own_id; + + bindtextdomain (GETTEXT_PACKAGE, GNOME_SETTINGS_LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + setlocale (LC_ALL, ""); + + set_empty_gtk_theme (TRUE); + +#ifdef GDK_BACKEND + { + const gchar *setup_display = getenv ("GNOME_SETUP_DISPLAY"); + if (setup_display && *setup_display != '\0') + g_setenv ("DISPLAY", setup_display, TRUE); + } + + gdk_set_allowed_backends (GDK_BACKEND); +#endif + + error = NULL; + if (! gtk_init_with_args (&argc, &argv, PLUGIN_NAME, entries, NULL, &error)) { + if (error != NULL) { + fprintf (stderr, "%s\n", error->message); + g_error_free (error); + } + exit (1); + } + + set_empty_gtk_theme (FALSE); + + if (verbose) { + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); + /* Work around GLib not flushing the output for us by explicitly + * setting buffering to a sane behaviour. This is important + * during testing when the output is not going to a TTY and + * we are reading messages from g_debug on stdout. + * + * See also + * https://bugzilla.gnome.org/show_bug.cgi?id=792432 + */ + setlinebuf (stdout); + } + + if (timeout > 0) { + guint id; + id = g_timeout_add_seconds (timeout, (GSourceFunc) gtk_main_quit, NULL); + g_source_set_name_by_id (id, "[gnome-settings-daemon] gtk_main_quit"); + } + + install_signal_handler (); + + manager = NEW (); + register_with_gnome_session (); + + if (!START (manager, &error)) { + fprintf (stderr, "Failed to start: %s\n", error->message); + g_error_free (error); + exit (1); + } + + name_own_id = g_bus_own_name (G_BUS_TYPE_SESSION, + PLUGIN_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE, + bus_acquired_cb, + name_acquired_cb, + name_lost_cb, + NULL, /* user_data */ + NULL /* user_data_free_func */); + + gtk_main (); + + STOP (manager); + + g_object_unref (manager); + g_bus_unown_name (name_own_id); + + return 0; +} diff --git a/plugins/common/daemon-skeleton.h b/plugins/common/daemon-skeleton.h new file mode 100644 index 0000000..40b7f14 --- /dev/null +++ b/plugins/common/daemon-skeleton.h @@ -0,0 +1,264 @@ +/** + * Create a gnome-settings-daemon helper easily + * + * #define NEW gsd_media_keys_manager_new + * #define START gsd_media_keys_manager_start + * #define MANAGER GsdMediaKeysManager + * #include "gsd-media-keys-manager.h" + * + * #include "daemon-skeleton.h" + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <locale.h> + +#include <glib-unix.h> +#include <glib/gi18n.h> + +#include "gnome-settings-bus.h" + +#ifndef PLUGIN_NAME +#error Include PLUGIN_CFLAGS in the daemon s CFLAGS +#endif /* !PLUGIN_NAME */ + +#ifndef PLUGIN_DBUS_NAME +#error Include PLUGIN_DBUS_NAME in the daemon s CFLAGS +#endif /* !PLUGIN_DBUS_NAME */ + +#define GNOME_SESSION_DBUS_NAME "org.gnome.SessionManager" +#define GNOME_SESSION_CLIENT_PRIVATE_DBUS_INTERFACE "org.gnome.SessionManager.ClientPrivate" + +static MANAGER *manager = NULL; +static int timeout = -1; +static char *dummy_name = NULL; +static gboolean verbose = FALSE; + +static GOptionEntry entries[] = { + { "exit-time", 0, 0, G_OPTION_ARG_INT, &timeout, "Exit after n seconds time", NULL }, + { "dummy-name", 0, 0, G_OPTION_ARG_STRING, &dummy_name, "Name when using the dummy daemon", NULL }, + { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "Verbose", NULL }, + {NULL} +}; + +static void +respond_to_end_session (GDBusProxy *proxy) +{ + /* we must answer with "EndSessionResponse" */ + g_dbus_proxy_call (proxy, "EndSessionResponse", + g_variant_new ("(bs)", TRUE, ""), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL, NULL); +} + +static void +do_stop (GMainLoop *loop) +{ + g_main_loop_quit (loop); +} + +static void +client_proxy_signal_cb (GDBusProxy *proxy, + gchar *sender_name, + gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + if (g_strcmp0 (signal_name, "QueryEndSession") == 0) { + g_debug ("Got QueryEndSession signal"); + respond_to_end_session (proxy); + } else if (g_strcmp0 (signal_name, "EndSession") == 0) { + g_debug ("Got EndSession signal"); + respond_to_end_session (proxy); + } else if (g_strcmp0 (signal_name, "Stop") == 0) { + g_debug ("Got Stop signal"); + do_stop (user_data); + } +} + +static void +on_client_registered (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GVariant *variant; + GMainLoop *loop = user_data; + GDBusProxy *client_proxy; + GError *error = NULL; + gchar *object_path = NULL; + + variant = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), res, &error); + if (!variant) { + g_warning ("Unable to register client: %s", error->message); + g_error_free (error); + return; + } + + g_variant_get (variant, "(o)", &object_path); + + g_debug ("Registered client at path %s", object_path); + + client_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, 0, NULL, + GNOME_SESSION_DBUS_NAME, + object_path, + GNOME_SESSION_CLIENT_PRIVATE_DBUS_INTERFACE, + NULL, + &error); + if (!client_proxy) { + g_warning ("Unable to get the session client proxy: %s", error->message); + g_error_free (error); + return; + } + + g_signal_connect (client_proxy, "g-signal", + G_CALLBACK (client_proxy_signal_cb), loop); + + g_free (object_path); + g_variant_unref (variant); +} + +static void +register_with_gnome_session (GMainLoop *loop) +{ + GDBusProxy *proxy; + const char *startup_id; + + proxy = G_DBUS_PROXY (gnome_settings_bus_get_session_proxy ()); + startup_id = g_getenv ("DESKTOP_AUTOSTART_ID"); + g_dbus_proxy_call (proxy, + "RegisterClient", + g_variant_new ("(ss)", dummy_name ? dummy_name : PLUGIN_NAME, startup_id ? startup_id : ""), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + (GAsyncReadyCallback) on_client_registered, + loop); + + /* DESKTOP_AUTOSTART_ID must not leak into child processes, because + * it can't be reused. Child processes will not know whether this is + * a genuine value or erroneous already-used value. */ + g_unsetenv ("DESKTOP_AUTOSTART_ID"); +} + +static gboolean +handle_sigterm (gpointer user_data) +{ + GMainLoop *main_loop = user_data; + + g_debug ("Got SIGTERM; shutting down ..."); + + if (g_main_loop_is_running (main_loop)) + g_main_loop_quit (main_loop); + + return G_SOURCE_REMOVE; +} + +static void +install_signal_handler (GMainLoop *loop) +{ + g_autoptr(GSource) source = NULL; + + source = g_unix_signal_source_new (SIGTERM); + + g_source_set_callback (source, handle_sigterm, loop, NULL); + g_source_attach (source, NULL); +} + +static void +bus_acquired_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data G_GNUC_UNUSED) +{ + g_debug ("%s: acquired bus %p for name %s", G_STRFUNC, connection, name); +} + +static void +name_acquired_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data G_GNUC_UNUSED) +{ + g_debug ("%s: acquired name %s on bus %p", G_STRFUNC, name, connection); +} + +static void +name_lost_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data G_GNUC_UNUSED) +{ + g_debug ("%s: lost name %s on bus %p", G_STRFUNC, name, connection); +} + +int +main (int argc, char **argv) +{ + GError *error = NULL; + GOptionContext *context; + GMainLoop *loop; + guint name_own_id; + + bindtextdomain (GETTEXT_PACKAGE, GNOME_SETTINGS_LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + setlocale (LC_ALL, ""); + + context = g_option_context_new (NULL); + g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); + if (!g_option_context_parse (context, &argc, &argv, &error)) { + fprintf (stderr, "%s\n", error->message); + g_error_free (error); + exit (1); + } + g_option_context_free (context); + + loop = g_main_loop_new (NULL, FALSE); + + if (verbose) { + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); + /* Work around GLib not flushing the output for us by explicitly + * setting buffering to a sane behaviour. This is important + * during testing when the output is not going to a TTY and + * we are reading messages from g_debug on stdout. + * + * See also + * https://bugzilla.gnome.org/show_bug.cgi?id=792432 + */ + setlinebuf (stdout); + } + + if (timeout > 0) { + guint id; + id = g_timeout_add_seconds (timeout, (GSourceFunc) g_main_loop_quit, loop); + g_source_set_name_by_id (id, "[gnome-settings-daemon] g_main_loop_quit"); + } + + install_signal_handler (loop); + + manager = NEW (); + register_with_gnome_session (loop); + + if (!START (manager, &error)) { + fprintf (stderr, "Failed to start: %s\n", error->message); + g_error_free (error); + exit (1); + } + + name_own_id = g_bus_own_name (G_BUS_TYPE_SESSION, + PLUGIN_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE, + bus_acquired_cb, + name_acquired_cb, + name_lost_cb, + NULL, /* user_data */ + NULL /* user_data_free_func */); + + g_main_loop_run (loop); + + STOP (manager); + + g_object_unref (manager); + g_bus_unown_name (name_own_id); + + return 0; +} diff --git a/plugins/common/gsd-input-helper.c b/plugins/common/gsd-input-helper.c new file mode 100644 index 0000000..d806ccf --- /dev/null +++ b/plugins/common/gsd-input-helper.c @@ -0,0 +1,79 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2010 Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <string.h> + +#include <gdk/gdk.h> +#include <gdk/gdkx.h> + +#include <sys/types.h> +#include <X11/Xatom.h> +#include <X11/extensions/XInput2.h> + +#include "gsd-input-helper.h" + +char * +xdevice_get_device_node (int deviceid) +{ + Atom prop; + Atom act_type; + int act_format; + unsigned long nitems, bytes_after; + unsigned char *data; + char *ret; + + gdk_display_sync (gdk_display_get_default ()); + + prop = XInternAtom (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), "Device Node", False); + if (!prop) + return NULL; + + gdk_x11_display_error_trap_push (gdk_display_get_default ()); + + if (!XIGetProperty (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), + deviceid, prop, 0, 1000, False, + AnyPropertyType, &act_type, &act_format, + &nitems, &bytes_after, &data) == Success) { + gdk_x11_display_error_trap_pop_ignored (gdk_display_get_default ()); + return NULL; + } + if (gdk_x11_display_error_trap_pop (gdk_display_get_default ())) + goto out; + + if (nitems == 0) + goto out; + + if (act_type != XA_STRING) + goto out; + + /* Unknown string format */ + if (act_format != 8) + goto out; + + ret = g_strdup ((char *) data); + + XFree (data); + return ret; + +out: + XFree (data); + return NULL; +} diff --git a/plugins/common/gsd-input-helper.h b/plugins/common/gsd-input-helper.h new file mode 100644 index 0000000..ae0d3ab --- /dev/null +++ b/plugins/common/gsd-input-helper.h @@ -0,0 +1,33 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2010 Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __GSD_INPUT_HELPER_H +#define __GSD_INPUT_HELPER_H + +G_BEGIN_DECLS + +#include <glib.h> + +#include <X11/extensions/XInput.h> +#include <X11/extensions/XIproto.h> + +char * xdevice_get_device_node (int deviceid); + +G_END_DECLS + +#endif /* __GSD_INPUT_HELPER_H */ diff --git a/plugins/common/gsd-settings-migrate.c b/plugins/common/gsd-settings-migrate.c new file mode 100644 index 0000000..a0e5cb8 --- /dev/null +++ b/plugins/common/gsd-settings-migrate.c @@ -0,0 +1,74 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2015 Red Hat + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * Author: Carlos Garnacho <carlosg@gnome.org> + */ + +#include "config.h" + +#include <gio/gio.h> + +#include "gsd-settings-migrate.h" + +void +gsd_settings_migrate_check (const gchar *origin_schema, + const gchar *origin_path, + const gchar *dest_schema, + const gchar *dest_path, + GsdSettingsMigrateEntry entries[], + guint n_entries) +{ + GSettings *origin_settings, *dest_settings; + guint i; + + origin_settings = g_settings_new_with_path (origin_schema, origin_path); + dest_settings = g_settings_new_with_path (dest_schema, dest_path); + + for (i = 0; i < n_entries; i++) { + g_autoptr(GVariant) variant = NULL; + + variant = g_settings_get_user_value (origin_settings, entries[i].origin_key); + + if (!variant) + continue; + + g_settings_reset (origin_settings, entries[i].origin_key); + + if (entries[i].dest_key) { + if (entries[i].func) { + g_autoptr(GVariant) old_default = NULL; + g_autoptr(GVariant) new_default = NULL; + GVariant *modified; + + old_default = g_settings_get_default_value (origin_settings, entries[i].origin_key); + new_default = g_settings_get_default_value (dest_settings, entries[i].dest_key); + + modified = entries[i].func (variant, old_default, new_default); + g_clear_pointer (&variant, g_variant_unref); + if (modified) + variant = g_variant_ref_sink (modified); + } + + if (variant) + g_settings_set_value (dest_settings, entries[i].dest_key, variant); + } + } + + g_object_unref (origin_settings); + g_object_unref (dest_settings); +} diff --git a/plugins/common/gsd-settings-migrate.h b/plugins/common/gsd-settings-migrate.h new file mode 100644 index 0000000..b8d2259 --- /dev/null +++ b/plugins/common/gsd-settings-migrate.h @@ -0,0 +1,43 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2015 Red Hat + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * Author: Carlos Garnacho <carlosg@gnome.org> + */ + +#ifndef __GSD_SETTINGS_MIGRATE_H__ +#define __GSD_SETTINGS_MIGRATE_H__ + +typedef struct _GsdSettingsMigrateEntry GsdSettingsMigrateEntry; + +typedef GVariant * (* GsdSettingsMigrateFunc) (GVariant *variant, GVariant *old_default, GVariant *new_default); + +struct _GsdSettingsMigrateEntry +{ + const gchar *origin_key; + const gchar *dest_key; + GsdSettingsMigrateFunc func; +}; + +void gsd_settings_migrate_check (const gchar *origin_schema, + const gchar *origin_path, + const gchar *dest_schema, + const gchar *dest_path, + GsdSettingsMigrateEntry entries[], + guint n_entries); + +#endif /* __GSD_SETTINGS_MIGRATE_H__ */ diff --git a/plugins/common/gsd-shell-helper.c b/plugins/common/gsd-shell-helper.c new file mode 100644 index 0000000..e4a2f23 --- /dev/null +++ b/plugins/common/gsd-shell-helper.c @@ -0,0 +1,66 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Carlos Garnacho <carlosg@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" +#include "gsd-shell-helper.h" + +void +shell_show_osd (GsdShell *shell, + const gchar *icon_name, + const gchar *label, + double level, + const gchar *connector) +{ + shell_show_osd_with_max_level (shell, icon_name, label, level, -1, connector); +} + +void +shell_show_osd_with_max_level (GsdShell *shell, + const gchar *icon_name, + const gchar *label, + double level, + double max_level, + const gchar *connector) +{ + GVariantBuilder builder; + + g_return_if_fail (GSD_IS_SHELL (shell)); + + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + + if (icon_name) + g_variant_builder_add (&builder, "{sv}", + "icon", g_variant_new_string (icon_name)); + if (label) + g_variant_builder_add (&builder, "{sv}", + "label", g_variant_new_string (label)); + if (level >= 0.0) + g_variant_builder_add (&builder, "{sv}", + "level", g_variant_new_double (level)); + if (max_level > 1.0) + g_variant_builder_add (&builder, "{sv}", + "max_level", g_variant_new_double (max_level)); + if (connector) + g_variant_builder_add (&builder, "{sv}", + "connector", g_variant_new_string (connector)); + + gsd_shell_call_show_osd (shell, + g_variant_builder_end (&builder), + NULL, NULL, NULL); +} diff --git a/plugins/common/gsd-shell-helper.h b/plugins/common/gsd-shell-helper.h new file mode 100644 index 0000000..16a694b --- /dev/null +++ b/plugins/common/gsd-shell-helper.h @@ -0,0 +1,42 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Carlos Garnacho <carlosg@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_SHELL_HELPER_H__ +#define __GSD_SHELL_HELPER_H__ + +#include "gsd-shell-glue.h" + +G_BEGIN_DECLS + +void shell_show_osd (GsdShell *shell, + const gchar *icon_name, + const gchar *label, + double level, + const gchar *connector); + +void shell_show_osd_with_max_level (GsdShell *shell, + const gchar *icon_name, + const gchar *label, + double level, + double max_level, + const gchar *connector); + +G_END_DECLS + +#endif /* __GSD_SHELL_HELPER_H__ */ diff --git a/plugins/common/gsd.gresources.xml b/plugins/common/gsd.gresources.xml new file mode 100644 index 0000000..e4ac1cd --- /dev/null +++ b/plugins/common/gsd.gresources.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gtk/libgtk/theme/Disabled"> + <file>gtk.css</file> + </gresource> +</gresources> diff --git a/plugins/common/gtk.css b/plugins/common/gtk.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/common/gtk.css diff --git a/plugins/common/meson.build b/plugins/common/meson.build new file mode 100644 index 0000000..2f18c2c --- /dev/null +++ b/plugins/common/meson.build @@ -0,0 +1,43 @@ +common_inc = include_directories('.') + +sources = files( + 'gsd-input-helper.c', + 'gsd-settings-migrate.c', + 'gsd-shell-helper.c' +) + +resource_data = files('gtk.css') + +sources += gnome.compile_resources( + 'gsd-resources', + 'gsd.gresources.xml', + c_name: 'gsd', + dependencies: resource_data +) + +deps = plugins_deps + [ + gnome_desktop_dep, + gtk_x11_dep, + x11_dep, + dependency('kbproto'), + dependency('xi') +] + +ldflags = [] +if host_is_darwin + ldflags += ['-Wl,-bundle_loader,@0@'.format(join_paths(), meson.build_root(), meson.project_name(), meson.project_name())] +endif + +libcommon = static_library( + plugin_name, + sources: sources, + include_directories: [top_inc, data_inc], + dependencies: deps, + c_args: cflags, + link_args: ldflags +) + +libcommon_dep = declare_dependency( + include_directories: common_inc, + link_with: libcommon +) diff --git a/plugins/datetime/backward b/plugins/datetime/backward new file mode 100644 index 0000000..f1f95a8 --- /dev/null +++ b/plugins/datetime/backward @@ -0,0 +1,118 @@ +# <pre> +# @(#)backward 8.9 +# This file is in the public domain, so clarified as of +# 2009-05-17 by Arthur David Olson. + +# This file provides links between current names for time zones +# and their old names. Many names changed in late 1993. + +Link Africa/Asmara Africa/Asmera +Link Africa/Bamako Africa/Timbuktu +Link America/Argentina/Catamarca America/Argentina/ComodRivadavia +Link America/Adak America/Atka +Link America/Argentina/Buenos_Aires America/Buenos_Aires +Link America/Argentina/Catamarca America/Catamarca +Link America/Atikokan America/Coral_Harbour +Link America/Argentina/Cordoba America/Cordoba +Link America/Tijuana America/Ensenada +Link America/Indiana/Indianapolis America/Fort_Wayne +Link America/Indiana/Indianapolis America/Indianapolis +Link America/Argentina/Jujuy America/Jujuy +Link America/Indiana/Knox America/Knox_IN +Link America/Kentucky/Louisville America/Louisville +Link America/Argentina/Mendoza America/Mendoza +Link America/Rio_Branco America/Porto_Acre +Link America/Argentina/Cordoba America/Rosario +Link America/St_Thomas America/Virgin +Link Asia/Ashgabat Asia/Ashkhabad +Link Asia/Chongqing Asia/Chungking +Link Asia/Dhaka Asia/Dacca +Link Asia/Kathmandu Asia/Katmandu +Link Asia/Kolkata Asia/Calcutta +Link Asia/Macau Asia/Macao +Link Asia/Jerusalem Asia/Tel_Aviv +Link Asia/Ho_Chi_Minh Asia/Saigon +Link Asia/Thimphu Asia/Thimbu +Link Asia/Makassar Asia/Ujung_Pandang +Link Asia/Ulaanbaatar Asia/Ulan_Bator +Link Atlantic/Faroe Atlantic/Faeroe +Link Europe/Oslo Atlantic/Jan_Mayen +Link Australia/Sydney Australia/ACT +Link Australia/Sydney Australia/Canberra +Link Australia/Lord_Howe Australia/LHI +Link Australia/Sydney Australia/NSW +Link Australia/Darwin Australia/North +Link Australia/Brisbane Australia/Queensland +Link Australia/Adelaide Australia/South +Link Australia/Hobart Australia/Tasmania +Link Australia/Melbourne Australia/Victoria +Link Australia/Perth Australia/West +Link Australia/Broken_Hill Australia/Yancowinna +Link America/Rio_Branco Brazil/Acre +Link America/Noronha Brazil/DeNoronha +Link America/Sao_Paulo Brazil/East +Link America/Manaus Brazil/West +Link America/Halifax Canada/Atlantic +Link America/Winnipeg Canada/Central +Link America/Regina Canada/East-Saskatchewan +Link America/Toronto Canada/Eastern +Link America/Edmonton Canada/Mountain +Link America/St_Johns Canada/Newfoundland +Link America/Vancouver Canada/Pacific +Link America/Regina Canada/Saskatchewan +Link America/Whitehorse Canada/Yukon +Link America/Santiago Chile/Continental +Link Pacific/Easter Chile/EasterIsland +Link America/Havana Cuba +Link Africa/Cairo Egypt +Link Europe/Dublin Eire +Link Europe/London Europe/Belfast +Link Europe/Chisinau Europe/Tiraspol +Link Europe/London GB +Link Europe/London GB-Eire +Link Etc/GMT GMT+0 +Link Etc/GMT GMT-0 +Link Etc/GMT GMT0 +Link Etc/GMT Greenwich +Link Asia/Hong_Kong Hongkong +Link Atlantic/Reykjavik Iceland +Link Asia/Tehran Iran +Link Asia/Jerusalem Israel +Link America/Jamaica Jamaica +Link Asia/Tokyo Japan +Link Pacific/Kwajalein Kwajalein +Link Africa/Tripoli Libya +Link America/Tijuana Mexico/BajaNorte +Link America/Mazatlan Mexico/BajaSur +Link America/Mexico_City Mexico/General +Link Pacific/Auckland NZ +Link Pacific/Chatham NZ-CHAT +Link America/Denver Navajo +Link Asia/Shanghai PRC +Link Pacific/Pago_Pago Pacific/Samoa +Link Pacific/Chuuk Pacific/Yap +Link Pacific/Chuuk Pacific/Truk +Link Pacific/Pohnpei Pacific/Ponape +Link Europe/Warsaw Poland +Link Europe/Lisbon Portugal +Link Asia/Taipei ROC +Link Asia/Seoul ROK +Link Asia/Singapore Singapore +Link Europe/Istanbul Turkey +Link Etc/UCT UCT +Link America/Anchorage US/Alaska +Link America/Adak US/Aleutian +Link America/Phoenix US/Arizona +Link America/Chicago US/Central +Link America/Indiana/Indianapolis US/East-Indiana +Link America/New_York US/Eastern +Link Pacific/Honolulu US/Hawaii +Link America/Indiana/Knox US/Indiana-Starke +Link America/Detroit US/Michigan +Link America/Denver US/Mountain +Link America/Los_Angeles US/Pacific +Link Pacific/Pago_Pago US/Samoa +Link Etc/UTC UTC +Link Etc/UTC Universal +Link Europe/Moscow W-SU +Link Etc/UTC Zulu diff --git a/plugins/datetime/gsd-datetime-manager.c b/plugins/datetime/gsd-datetime-manager.c new file mode 100644 index 0000000..dcd9f8c --- /dev/null +++ b/plugins/datetime/gsd-datetime-manager.c @@ -0,0 +1,226 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Kalev Lember <kalevlember@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <gio/gio.h> +#include <glib/gi18n.h> +#include <libnotify/notify.h> + +#include "gsd-datetime-manager.h" +#include "gsd-timezone-monitor.h" +#include "gnome-settings-profile.h" + +#define DATETIME_SCHEMA "org.gnome.desktop.datetime" +#define AUTO_TIMEZONE_KEY "automatic-timezone" + +struct _GsdDatetimeManager +{ + GObject parent; + + GSettings *settings; + GsdTimezoneMonitor *timezone_monitor; + NotifyNotification *notification; +}; + +static void gsd_datetime_manager_class_init (GsdDatetimeManagerClass *klass); +static void gsd_datetime_manager_init (GsdDatetimeManager *manager); +static void gsd_datetime_manager_finalize (GObject *object); + +G_DEFINE_TYPE (GsdDatetimeManager, gsd_datetime_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +static void +notification_closed_cb (NotifyNotification *n, + GsdDatetimeManager *self) +{ + g_clear_object (&self->notification); +} + +static void +open_settings_cb (NotifyNotification *n, + const char *action, + const char *path) +{ + const gchar *argv[] = { "gnome-control-center", "datetime", NULL }; + + g_debug ("Running gnome-control-center datetime"); + g_spawn_async (NULL, (gchar **) argv, NULL, G_SPAWN_SEARCH_PATH, + NULL, NULL, NULL, NULL); + + notify_notification_close (n, NULL); +} + +static void +timezone_changed_cb (GsdTimezoneMonitor *timezone_monitor, + const gchar *timezone_id, + GsdDatetimeManager *self) +{ + GDateTime *datetime; + GTimeZone *tz; + gchar *notification_summary; + gchar *timezone_name; + gchar *utc_offset; + + tz = g_time_zone_new (timezone_id); + datetime = g_date_time_new_now (tz); + g_time_zone_unref (tz); + + /* Translators: UTC here means the Coordinated Universal Time. + * %:::z will be replaced by the offset from UTC e.g. UTC+02 */ + utc_offset = g_date_time_format (datetime, _("UTC%:::z")); + timezone_name = g_strdup (g_date_time_get_timezone_abbreviation (datetime)); + g_date_time_unref (datetime); + + notification_summary = g_strdup_printf (_("Time Zone Updated to %s (%s)"), + timezone_name, + utc_offset); + g_free (timezone_name); + g_free (utc_offset); + + if (self->notification == NULL) { + self->notification = notify_notification_new (notification_summary, NULL, + "preferences-system-time-symbolic"); + g_signal_connect (self->notification, + "closed", + G_CALLBACK (notification_closed_cb), + self); + + notify_notification_add_action (self->notification, + "settings", + _("Settings"), + (NotifyActionCallback) open_settings_cb, + NULL, NULL); + } else { + notify_notification_update (self->notification, + notification_summary, NULL, + "preferences-system-time-symbolic"); + } + g_free (notification_summary); + + notify_notification_set_app_name (self->notification, _("Date & Time Settings")); + notify_notification_set_hint_string (self->notification, "desktop-entry", "gnome-datetime-panel"); + notify_notification_set_urgency (self->notification, NOTIFY_URGENCY_NORMAL); + notify_notification_set_timeout (self->notification, NOTIFY_EXPIRES_NEVER); + + if (!notify_notification_show (self->notification, NULL)) { + g_warning ("Failed to send timezone notification"); + } +} + +static void +auto_timezone_settings_changed_cb (GSettings *settings, + const char *key, + GsdDatetimeManager *self) +{ + gboolean enabled; + + enabled = g_settings_get_boolean (settings, key); + if (enabled && self->timezone_monitor == NULL) { + g_debug ("Automatic timezone enabled"); + self->timezone_monitor = gsd_timezone_monitor_new (); + + g_signal_connect (self->timezone_monitor, "timezone-changed", + G_CALLBACK (timezone_changed_cb), self); + } else { + g_debug ("Automatic timezone disabled"); + g_clear_object (&self->timezone_monitor); + } +} + +gboolean +gsd_datetime_manager_start (GsdDatetimeManager *self, + GError **error) +{ + g_debug ("Starting datetime manager"); + gnome_settings_profile_start (NULL); + + self->settings = g_settings_new (DATETIME_SCHEMA); + + g_signal_connect (self->settings, "changed::" AUTO_TIMEZONE_KEY, + G_CALLBACK (auto_timezone_settings_changed_cb), self); + auto_timezone_settings_changed_cb (self->settings, AUTO_TIMEZONE_KEY, self); + + gnome_settings_profile_end (NULL); + + return TRUE; +} + +void +gsd_datetime_manager_stop (GsdDatetimeManager *self) +{ + g_debug ("Stopping datetime manager"); + + g_clear_object (&self->settings); + g_clear_object (&self->timezone_monitor); + + if (self->notification != NULL) { + g_signal_handlers_disconnect_by_func (self->notification, + G_CALLBACK (notification_closed_cb), + self); + g_clear_object (&self->notification); + } +} + +static void +gsd_datetime_manager_class_init (GsdDatetimeManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_datetime_manager_finalize; + + notify_init ("gnome-settings-daemon"); +} + +static void +gsd_datetime_manager_init (GsdDatetimeManager *manager) +{ +} + +static void +gsd_datetime_manager_finalize (GObject *object) +{ + GsdDatetimeManager *manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_DATETIME_MANAGER (object)); + + manager = GSD_DATETIME_MANAGER (object); + + g_return_if_fail (manager != NULL); + + gsd_datetime_manager_stop (manager); + + G_OBJECT_CLASS (gsd_datetime_manager_parent_class)->finalize (object); +} + +GsdDatetimeManager * +gsd_datetime_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_DATETIME_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_DATETIME_MANAGER (manager_object); +} diff --git a/plugins/datetime/gsd-datetime-manager.h b/plugins/datetime/gsd-datetime-manager.h new file mode 100644 index 0000000..5478145 --- /dev/null +++ b/plugins/datetime/gsd-datetime-manager.h @@ -0,0 +1,37 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Kalev Lember <kalevlember@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_DATETIME_MANAGER_H +#define __GSD_DATETIME_MANAGER_H + +#include <glib.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_DATETIME_MANAGER (gsd_datetime_manager_get_type ()) +G_DECLARE_FINAL_TYPE (GsdDatetimeManager, gsd_datetime_manager, GSD, DATETIME_MANAGER, GObject) + +GsdDatetimeManager *gsd_datetime_manager_new (void); +gboolean gsd_datetime_manager_start (GsdDatetimeManager *manager, GError **error); +void gsd_datetime_manager_stop (GsdDatetimeManager *manager); + +G_END_DECLS + +#endif /* __GSD_DATETIME_MANAGER_H */ diff --git a/plugins/datetime/gsd-timezone-monitor.c b/plugins/datetime/gsd-timezone-monitor.c new file mode 100644 index 0000000..a6e8a48 --- /dev/null +++ b/plugins/datetime/gsd-timezone-monitor.c @@ -0,0 +1,472 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Kalev Lember <kalevlember@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include "gsd-timezone-monitor.h" + +#include "timedated.h" +#include "tz.h" +#include "weather-tz.h" + +#include <geoclue.h> +#include <geocode-glib/geocode-glib.h> +#include <polkit/polkit.h> + +#define DESKTOP_ID "gnome-datetime-panel" +#define SET_TIMEZONE_PERMISSION "org.freedesktop.timedate1.set-timezone" + +enum { + TIMEZONE_CHANGED, + LAST_SIGNAL +}; + +static int signals[LAST_SIGNAL] = { 0 }; + +typedef struct +{ + GCancellable *cancellable; + GPermission *permission; + Timedate1 *dtm; + + GClueClient *geoclue_client; + GClueSimple *geoclue_simple; + GCancellable *geoclue_cancellable; + + gchar *current_timezone; + + GSettings *location_settings; +} GsdTimezoneMonitorPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsdTimezoneMonitor, gsd_timezone_monitor, G_TYPE_OBJECT) + +static void +set_timezone_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsdTimezoneMonitorPrivate *priv; + GError *error = NULL; + + if (!timedate1_call_set_timezone_finish (TIMEDATE1 (source), + res, + &error)) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Could not set system timezone: %s", error->message); + g_error_free (error); + return; + } + + priv = gsd_timezone_monitor_get_instance_private (user_data); + g_signal_emit (G_OBJECT (user_data), + signals[TIMEZONE_CHANGED], + 0, priv->current_timezone); + + g_debug ("Successfully changed timezone to '%s'", + priv->current_timezone); +} + +static void +queue_set_timezone (GsdTimezoneMonitor *self, + const gchar *new_timezone) +{ + GsdTimezoneMonitorPrivate *priv = gsd_timezone_monitor_get_instance_private (self); + + g_debug ("Changing timezone to '%s'", new_timezone); + + timedate1_call_set_timezone (priv->dtm, + new_timezone, + TRUE, + priv->cancellable, + set_timezone_cb, + self); + + g_free (priv->current_timezone); + priv->current_timezone = g_strdup (new_timezone); +} + +static gint +compare_locations (TzLocation *a, + TzLocation *b) +{ + if (a->dist > b->dist) + return 1; + + if (a->dist < b->dist) + return -1; + + return 0; +} + +static GList * +sort_by_closest_to (GList *locations, + GeocodeLocation *location) +{ + GList *l; + + for (l = locations; l; l = l->next) { + GeocodeLocation *loc; + TzLocation *tz_location = l->data; + + loc = geocode_location_new (tz_location->latitude, + tz_location->longitude, + GEOCODE_LOCATION_ACCURACY_UNKNOWN); + tz_location->dist = geocode_location_get_distance_from (loc, location); + g_object_unref (loc); + } + + return g_list_sort (locations, (GCompareFunc) compare_locations); +} + +static GList * +ptr_array_to_list (GPtrArray *array) +{ + GList *l = NULL; + gint i; + + for (i = 0; i < array->len; i++) + l = g_list_prepend (l, g_ptr_array_index (array, i)); + + return l; +} + +static GList * +find_by_country (GList *locations, + const gchar *country_code) +{ + GList *l, *found = NULL; + gchar *c1; + gchar *c2; + + c1 = g_ascii_strdown (country_code, -1); + + for (l = locations; l; l = l->next) { + TzLocation *loc = l->data; + + c2 = g_ascii_strdown (loc->country, -1); + if (g_strcmp0 (c1, c2) == 0) + found = g_list_prepend (found, loc); + g_free (c2); + } + g_free (c1); + + return found; +} + +static gchar * +find_timezone (GsdTimezoneMonitor *self, + GeocodeLocation *location, + const gchar *country_code) +{ + TzDB *tzdb; + gchar *res; + GList *filtered; + GList *weather_locations; + GList *locations; + TzLocation *closest_tz_location; + + tzdb = tz_load_db (); + + /* First load locations from Olson DB */ + locations = ptr_array_to_list (tz_get_locations (tzdb)); + g_return_val_if_fail (locations != NULL, NULL); + + /* ... and then add libgweather's locations as well */ + weather_locations = weather_tz_db_get_locations (country_code); + locations = g_list_concat (locations, + g_list_copy (weather_locations)); + + /* Filter tz locations by country */ + filtered = find_by_country (locations, country_code); + if (filtered != NULL) { + g_list_free (locations); + locations = filtered; + } else { + g_debug ("No match for country code '%s' in tzdb", country_code); + } + + /* Find the closest tz location */ + locations = sort_by_closest_to (locations, location); + closest_tz_location = (TzLocation *) locations->data; + + res = g_strdup (closest_tz_location->zone); + + g_list_free (locations); + g_list_free_full (weather_locations, (GDestroyNotify) tz_location_free); + tz_db_free (tzdb); + + return res; +} + +static void +process_location (GsdTimezoneMonitor *self, + GeocodePlace *place) +{ + GeocodeLocation *location; + GsdTimezoneMonitorPrivate *priv = gsd_timezone_monitor_get_instance_private (self); + const gchar *country_code; + g_autofree gchar *new_timezone = NULL; + + country_code = geocode_place_get_country_code (place); + location = geocode_place_get_location (place); + + new_timezone = find_timezone (self, location, country_code); + + if (g_strcmp0 (priv->current_timezone, new_timezone) != 0) { + g_debug ("Found updated timezone '%s' for country '%s'", + new_timezone, country_code); + queue_set_timezone (self, new_timezone); + } else { + g_debug ("Timezone didn't change from '%s' for country '%s'", + new_timezone, country_code); + } +} + +static void +on_reverse_geocoding_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GeocodePlace *place; + GError *error = NULL; + + place = geocode_reverse_resolve_finish (GEOCODE_REVERSE (source_object), + res, + &error); + if (error != NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Reverse geocoding failed: %s", error->message); + g_error_free (error); + return; + } + g_debug ("Geocode lookup resolved country to '%s'", + geocode_place_get_country (place)); + + process_location (user_data, place); + g_object_unref (place); +} + +static void +start_reverse_geocoding (GsdTimezoneMonitor *self, + gdouble latitude, + gdouble longitude) +{ + GeocodeLocation *location; + GeocodeReverse *reverse; + GsdTimezoneMonitorPrivate *priv = gsd_timezone_monitor_get_instance_private (self); + + location = geocode_location_new (latitude, + longitude, + GEOCODE_LOCATION_ACCURACY_CITY); + + reverse = geocode_reverse_new_for_location (location); + geocode_reverse_resolve_async (reverse, + priv->geoclue_cancellable, + on_reverse_geocoding_ready, + self); + + g_object_unref (location); + g_object_unref (reverse); +} + +static void +on_location_notify (GClueSimple *simple, + GParamSpec *pspec, + gpointer user_data) +{ + GsdTimezoneMonitor *self = user_data; + GClueLocation *location; + gdouble latitude, longitude; + + location = gclue_simple_get_location (simple); + + latitude = gclue_location_get_latitude (location); + longitude = gclue_location_get_longitude (location); + + g_debug ("Got location %lf,%lf", latitude, longitude); + + start_reverse_geocoding (self, latitude, longitude); +} + +static void +on_geoclue_simple_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GError *error = NULL; + GsdTimezoneMonitorPrivate *priv; + GClueSimple *geoclue_simple; + + geoclue_simple = gclue_simple_new_finish (res, &error); + if (geoclue_simple == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to connect to GeoClue2 service: %s", error->message); + g_error_free (error); + return; + } + + g_debug ("Geoclue now available"); + + priv = gsd_timezone_monitor_get_instance_private (user_data); + priv->geoclue_simple = geoclue_simple; + priv->geoclue_client = gclue_simple_get_client (priv->geoclue_simple); + gclue_client_set_distance_threshold (priv->geoclue_client, + GEOCODE_LOCATION_ACCURACY_CITY); + + g_signal_connect (priv->geoclue_simple, "notify::location", + G_CALLBACK (on_location_notify), user_data); + + on_location_notify (priv->geoclue_simple, NULL, user_data); +} + +static void +start_geoclue (GsdTimezoneMonitor *self) +{ + GsdTimezoneMonitorPrivate *priv = gsd_timezone_monitor_get_instance_private (self); + + g_debug ("Timezone monitor enabled, starting geoclue"); + + priv->geoclue_cancellable = g_cancellable_new (); + gclue_simple_new (DESKTOP_ID, + GCLUE_ACCURACY_LEVEL_CITY, + priv->geoclue_cancellable, + on_geoclue_simple_ready, + self); + +} + +static void +stop_geoclue (GsdTimezoneMonitor *self) +{ + GsdTimezoneMonitorPrivate *priv = gsd_timezone_monitor_get_instance_private (self); + + g_debug ("Timezone monitor disabled, stopping geoclue"); + + g_cancellable_cancel (priv->geoclue_cancellable); + g_clear_object (&priv->geoclue_cancellable); + + if (priv->geoclue_client) { + gclue_client_call_stop (priv->geoclue_client, NULL, NULL, NULL); + priv->geoclue_client = NULL; + } + + g_clear_object (&priv->geoclue_simple); +} + +GsdTimezoneMonitor * +gsd_timezone_monitor_new (void) +{ + return g_object_new (GSD_TYPE_TIMEZONE_MONITOR, NULL); +} + +static void +gsd_timezone_monitor_finalize (GObject *obj) +{ + GsdTimezoneMonitor *monitor = GSD_TIMEZONE_MONITOR (obj); + GsdTimezoneMonitorPrivate *priv = gsd_timezone_monitor_get_instance_private (monitor); + + g_debug ("Stopping timezone monitor"); + + stop_geoclue (monitor); + + if (priv->cancellable) { + g_cancellable_cancel (priv->cancellable); + g_clear_object (&priv->cancellable); + } + + g_clear_object (&priv->dtm); + g_clear_object (&priv->permission); + g_clear_pointer (&priv->current_timezone, g_free); + + g_clear_object (&priv->location_settings); + + G_OBJECT_CLASS (gsd_timezone_monitor_parent_class)->finalize (obj); +} + +static void +gsd_timezone_monitor_class_init (GsdTimezoneMonitorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_timezone_monitor_finalize; + + signals[TIMEZONE_CHANGED] = + g_signal_new ("timezone-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsdTimezoneMonitorClass, timezone_changed), + NULL, NULL, + NULL, + G_TYPE_NONE, 1, G_TYPE_POINTER); +} + +static void +check_location_settings (GsdTimezoneMonitor *self) +{ + GsdTimezoneMonitorPrivate *priv = gsd_timezone_monitor_get_instance_private (self); + if (g_settings_get_boolean (priv->location_settings, "enabled")) + start_geoclue (self); + else + stop_geoclue (self); +} + +static void +gsd_timezone_monitor_init (GsdTimezoneMonitor *self) +{ + GError *error = NULL; + GsdTimezoneMonitorPrivate *priv = gsd_timezone_monitor_get_instance_private (self); + + g_debug ("Starting timezone monitor"); + + priv->permission = polkit_permission_new_sync (SET_TIMEZONE_PERMISSION, + NULL, NULL, + &error); + if (priv->permission == NULL) { + g_warning ("Could not get '%s' permission: %s", + SET_TIMEZONE_PERMISSION, + error->message); + g_error_free (error); + return; + } + + if (!g_permission_get_allowed (priv->permission)) { + g_debug ("No permission to set timezone"); + return; + } + + priv->cancellable = g_cancellable_new (); + priv->dtm = timedate1_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + "org.freedesktop.timedate1", + "/org/freedesktop/timedate1", + priv->cancellable, + &error); + if (priv->dtm == NULL) { + g_warning ("Could not get proxy for DateTimeMechanism: %s", error->message); + g_error_free (error); + return; + } + + priv->current_timezone = timedate1_dup_timezone (priv->dtm); + + priv->location_settings = g_settings_new ("org.gnome.system.location"); + g_signal_connect_swapped (priv->location_settings, "changed::enabled", + G_CALLBACK (check_location_settings), self); + check_location_settings (self); +} diff --git a/plugins/datetime/gsd-timezone-monitor.h b/plugins/datetime/gsd-timezone-monitor.h new file mode 100644 index 0000000..da2bcf8 --- /dev/null +++ b/plugins/datetime/gsd-timezone-monitor.h @@ -0,0 +1,55 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Kalev Lember <kalevlember@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_TIMEZONE_MONITOR_H +#define __GSD_TIMEZONE_MONITOR_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_TIMEZONE_MONITOR (gsd_timezone_monitor_get_type ()) +#define GSD_TIMEZONE_MONITOR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GSD_TYPE_TIMEZONE_MONITOR, GsdTimezoneMonitor)) +#define GSD_IS_TIMEZONE_MONITOR(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GSD_TYPE_TIMEZONE_MONITOR)) +#define GSD_TIMEZONE_MONITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GSD_TYPE_TIMEZONE_MONITOR, GsdTimezoneMonitorClass)) +#define GSD_IS_TIMEZONE_MONITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GSD_TYPE_TIMEZONE_MONITOR)) +#define GSD_TIMEZONE_MONITOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GSD_TYPE_TIMEZONE_MONITOR, GsdTimezoneMonitorClass)) + +typedef struct _GsdTimezoneMonitor GsdTimezoneMonitor; +typedef struct _GsdTimezoneMonitorClass GsdTimezoneMonitorClass; + +struct _GsdTimezoneMonitor +{ + GObject parent_instance; +}; + +struct _GsdTimezoneMonitorClass +{ + GObjectClass parent_class; + + void (*timezone_changed) (GsdTimezoneMonitor *monitor, gchar *timezone_id); +}; + +GType gsd_timezone_monitor_get_type (void) G_GNUC_CONST; + +GsdTimezoneMonitor *gsd_timezone_monitor_new (void); + +G_END_DECLS + +#endif /* __GSD_TIMEZONE_MONITOR_H */ diff --git a/plugins/datetime/main.c b/plugins/datetime/main.c new file mode 100644 index 0000000..8950ab0 --- /dev/null +++ b/plugins/datetime/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_datetime_manager_new +#define START gsd_datetime_manager_start +#define STOP gsd_datetime_manager_stop +#define MANAGER GsdDatetimeManager +#include "gsd-datetime-manager.h" + +#include "daemon-skeleton.h" diff --git a/plugins/datetime/meson.build b/plugins/datetime/meson.build new file mode 100644 index 0000000..ed2d433 --- /dev/null +++ b/plugins/datetime/meson.build @@ -0,0 +1,40 @@ +install_data( + 'backward', + install_dir: join_paths(gsd_pkgdatadir, 'datetime') +) + +sources = files( + 'gsd-datetime-manager.c', + 'gsd-timezone-monitor.c', + 'main.c', + 'tz.c', + 'weather-tz.c' +) + +sources += gnome.gdbus_codegen( + 'timedated', + 'timedated1-interface.xml', + interface_prefix: 'org.freedesktop.' +) + +deps = plugins_deps + [ + geocode_glib_dep, + gweather_dep, + libgeoclue_dep, + libnotify_dep, + m_dep, + polkit_gobject_dep +] + +cflags += ['-DGNOMECC_DATA_DIR="@0@"'.format(gsd_pkgdatadir)] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) diff --git a/plugins/datetime/timedated1-interface.xml b/plugins/datetime/timedated1-interface.xml new file mode 100644 index 0000000..3370e0e --- /dev/null +++ b/plugins/datetime/timedated1-interface.xml @@ -0,0 +1,28 @@ +<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> +<node> + <interface name="org.freedesktop.timedate1"> + <property name="Timezone" type="s" access="read"/> + <property name="LocalRTC" type="b" access="read"/> + <property name="CanNTP" type="b" access="read"/> + <property name="NTP" type="b" access="read"/> + <method name="SetTime"> + <arg name="usec_utc" type="x" direction="in"/> + <arg name="relative" type="b" direction="in"/> + <arg name="user_interaction" type="b" direction="in"/> + </method> + <method name="SetTimezone"> + <arg name="timezone" type="s" direction="in"/> + <arg name="user_interaction" type="b" direction="in"/> + </method> + <method name="SetLocalRTC"> + <arg name="local_rtc" type="b" direction="in"/> + <arg name="fix_system" type="b" direction="in"/> + <arg name="user_interaction" type="b" direction="in"/> + </method> + <method name="SetNTP"> + <arg name="use_ntp" type="b" direction="in"/> + <arg name="user_interaction" type="b" direction="in"/> + </method> + </interface> +</node> diff --git a/plugins/datetime/tz.c b/plugins/datetime/tz.c new file mode 100644 index 0000000..034d63d --- /dev/null +++ b/plugins/datetime/tz.c @@ -0,0 +1,482 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* Generic timezone utilities. + * + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Authors: Hans Petter Jansson <hpj@ximian.com> + * + * Largely based on Michael Fulbright's work on Anaconda. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + + +#include <glib.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <time.h> +#include <math.h> +#include <string.h> +#include <ctype.h> +#include "tz.h" + + +/* Forward declarations for private functions */ + +static float convert_pos (gchar *pos, int digits); +static int compare_country_names (const void *a, const void *b); +static void sort_locations_by_country (GPtrArray *locations); +static gchar * tz_data_file_get (void); +static void load_backward_tz (TzDB *tz_db); + +/* ---------------- * + * Public interface * + * ---------------- */ +TzDB * +tz_load_db (void) +{ + gchar *tz_data_file; + TzDB *tz_db; + FILE *tzfile; + char buf[4096]; + + tz_data_file = tz_data_file_get (); + if (!tz_data_file) { + g_warning ("Could not get the TimeZone data file name"); + return NULL; + } + tzfile = fopen (tz_data_file, "r"); + if (!tzfile) { + g_warning ("Could not open *%s*\n", tz_data_file); + g_free (tz_data_file); + return NULL; + } + + tz_db = g_new0 (TzDB, 1); + tz_db->locations = g_ptr_array_new (); + + while (fgets (buf, sizeof(buf), tzfile)) + { + gchar **tmpstrarr; + gchar *latstr, *lngstr, *p; + TzLocation *loc; + + if (*buf == '#') continue; + + g_strchomp(buf); + tmpstrarr = g_strsplit(buf,"\t", 6); + + latstr = g_strdup (tmpstrarr[1]); + p = latstr + 1; + while (*p != '-' && *p != '+') p++; + lngstr = g_strdup (p); + *p = '\0'; + + loc = g_new0 (TzLocation, 1); + loc->country = g_strdup (tmpstrarr[0]); + loc->zone = g_strdup (tmpstrarr[2]); + loc->latitude = convert_pos (latstr, 2); + loc->longitude = convert_pos (lngstr, 3); + +#ifdef __sun + if (tmpstrarr[3] && *tmpstrarr[3] == '-' && tmpstrarr[4]) + loc->comment = g_strdup (tmpstrarr[4]); + + if (tmpstrarr[3] && *tmpstrarr[3] != '-' && !islower(loc->zone)) { + TzLocation *locgrp; + + /* duplicate entry */ + locgrp = g_new0 (TzLocation, 1); + locgrp->country = g_strdup (tmpstrarr[0]); + locgrp->zone = g_strdup (tmpstrarr[3]); + locgrp->latitude = convert_pos (latstr, 2); + locgrp->longitude = convert_pos (lngstr, 3); + locgrp->comment = (tmpstrarr[4]) ? g_strdup (tmpstrarr[4]) : NULL; + + g_ptr_array_add (tz_db->locations, (gpointer) locgrp); + } +#else + loc->comment = (tmpstrarr[3]) ? g_strdup(tmpstrarr[3]) : NULL; +#endif + + g_ptr_array_add (tz_db->locations, (gpointer) loc); + + g_free (latstr); + g_free (lngstr); + g_strfreev (tmpstrarr); + } + + fclose (tzfile); + + /* now sort by country */ + sort_locations_by_country (tz_db->locations); + + g_free (tz_data_file); + + /* Load up the hashtable of backward links */ + load_backward_tz (tz_db); + + return tz_db; +} + +void +tz_location_free (TzLocation *loc) +{ + g_free (loc->country); + g_free (loc->zone); + g_free (loc->comment); + + g_free (loc); +} + +void +tz_db_free (TzDB *db) +{ + g_ptr_array_foreach (db->locations, (GFunc) tz_location_free, NULL); + g_ptr_array_free (db->locations, TRUE); + g_hash_table_destroy (db->backward); + g_free (db); +} + +GPtrArray * +tz_get_locations (TzDB *db) +{ + return db->locations; +} + + +gchar * +tz_location_get_country (TzLocation *loc) +{ + return loc->country; +} + + +gchar * +tz_location_get_zone (TzLocation *loc) +{ + return loc->zone; +} + + +gchar * +tz_location_get_comment (TzLocation *loc) +{ + return loc->comment; +} + + +void +tz_location_get_position (TzLocation *loc, double *longitude, double *latitude) +{ + *longitude = loc->longitude; + *latitude = loc->latitude; +} + +glong +tz_location_get_utc_offset (TzLocation *loc) +{ + TzInfo *tz_info; + glong offset; + + tz_info = tz_info_from_location (loc); + offset = tz_info->utc_offset; + tz_info_free (tz_info); + return offset; +} + +TzInfo * +tz_info_from_location (TzLocation *loc) +{ + TzInfo *tzinfo; + time_t curtime; + struct tm *curzone; + gchar *tz_env_value; + + g_return_val_if_fail (loc != NULL, NULL); + g_return_val_if_fail (loc->zone != NULL, NULL); + + tz_env_value = g_strdup (getenv ("TZ")); + setenv ("TZ", loc->zone, 1); + +#if 0 + tzset (); +#endif + tzinfo = g_new0 (TzInfo, 1); + + curtime = time (NULL); + curzone = localtime (&curtime); + +#ifndef __sun + /* Currently this solution doesnt seem to work - I get that */ + /* America/Phoenix uses daylight savings, which is wrong */ + tzinfo->tzname_normal = g_strdup (curzone->tm_zone); + if (curzone->tm_isdst) + tzinfo->tzname_daylight = + g_strdup (&curzone->tm_zone[curzone->tm_isdst]); + else + tzinfo->tzname_daylight = NULL; + + tzinfo->utc_offset = curzone->tm_gmtoff; +#else + tzinfo->tzname_normal = NULL; + tzinfo->tzname_daylight = NULL; + tzinfo->utc_offset = 0; +#endif + + tzinfo->daylight = curzone->tm_isdst; + + if (tz_env_value) + setenv ("TZ", tz_env_value, 1); + else + unsetenv ("TZ"); + + g_free (tz_env_value); + + return tzinfo; +} + + +void +tz_info_free (TzInfo *tzinfo) +{ + g_return_if_fail (tzinfo != NULL); + + if (tzinfo->tzname_normal) g_free (tzinfo->tzname_normal); + if (tzinfo->tzname_daylight) g_free (tzinfo->tzname_daylight); + g_free (tzinfo); +} + +struct { + const char *orig; + const char *dest; +} aliases[] = { + { "Asia/Istanbul", "Europe/Istanbul" }, /* Istanbul is in both Europe and Asia */ + { "Europe/Nicosia", "Asia/Nicosia" }, /* Ditto */ + { "EET", "Europe/Istanbul" }, /* Same tz as the 2 above */ + { "HST", "Pacific/Honolulu" }, + { "WET", "Europe/Brussels" }, /* Other name for the mainland Europe tz */ + { "CET", "Europe/Brussels" }, /* ditto */ + { "MET", "Europe/Brussels" }, + { "Etc/Zulu", "Etc/GMT" }, + { "Etc/UTC", "Etc/GMT" }, + { "GMT", "Etc/GMT" }, + { "Greenwich", "Etc/GMT" }, + { "Etc/UCT", "Etc/GMT" }, + { "Etc/GMT0", "Etc/GMT" }, + { "Etc/GMT+0", "Etc/GMT" }, + { "Etc/GMT-0", "Etc/GMT" }, + { "Etc/Universal", "Etc/GMT" }, + { "PST8PDT", "America/Los_Angeles" }, /* Other name for the Atlantic tz */ + { "EST", "America/New_York" }, /* Other name for the Eastern tz */ + { "EST5EDT", "America/New_York" }, /* ditto */ + { "CST6CDT", "America/Chicago" }, /* Other name for the Central tz */ + { "MST", "America/Denver" }, /* Other name for the mountain tz */ + { "MST7MDT", "America/Denver" }, /* ditto */ +}; + +static gboolean +compare_timezones (const char *a, + const char *b) +{ + if (g_str_equal (a, b)) + return TRUE; + if (strchr (b, '/') == NULL) { + char *prefixed; + + prefixed = g_strdup_printf ("/%s", b); + if (g_str_has_suffix (a, prefixed)) { + g_free (prefixed); + return TRUE; + } + g_free (prefixed); + } + + return FALSE; +} + +char * +tz_info_get_clean_name (TzDB *tz_db, + const char *tz) +{ + char *ret; + const char *timezone; + guint i; + gboolean replaced; + + /* Remove useless prefixes */ + if (g_str_has_prefix (tz, "right/")) + tz = tz + strlen ("right/"); + else if (g_str_has_prefix (tz, "posix/")) + tz = tz + strlen ("posix/"); + + /* Here start the crazies */ + replaced = FALSE; + + for (i = 0; i < G_N_ELEMENTS (aliases); i++) { + if (compare_timezones (tz, aliases[i].orig)) { + replaced = TRUE; + timezone = aliases[i].dest; + break; + } + } + + /* Try again! */ + if (!replaced) { + /* Ignore crazy solar times from the '80s */ + if (g_str_has_prefix (tz, "Asia/Riyadh") || + g_str_has_prefix (tz, "Mideast/Riyadh")) { + timezone = "Asia/Riyadh"; + replaced = TRUE; + } + } + + if (!replaced) + timezone = tz; + + ret = g_hash_table_lookup (tz_db->backward, timezone); + if (ret == NULL) + return g_strdup (timezone); + return g_strdup (ret); +} + +/* ----------------- * + * Private functions * + * ----------------- */ + +static gchar * +tz_data_file_get (void) +{ + gchar *file; + + file = g_strdup (TZ_DATA_FILE); + + return file; +} + +static float +convert_pos (gchar *pos, int digits) +{ + gchar whole[10]; + gchar *fraction; + gint i; + float t1, t2; + + if (!pos || strlen(pos) < 4 || digits > 9) return 0.0; + + for (i = 0; i < digits + 1; i++) whole[i] = pos[i]; + whole[i] = '\0'; + fraction = pos + digits + 1; + + t1 = g_strtod (whole, NULL); + t2 = g_strtod (fraction, NULL); + + if (t1 >= 0.0) return t1 + t2/pow (10.0, strlen(fraction)); + else return t1 - t2/pow (10.0, strlen(fraction)); +} + + +#if 0 + +/* Currently not working */ +static void +free_tzdata (TzLocation *tz) +{ + + if (tz->country) + g_free(tz->country); + if (tz->zone) + g_free(tz->zone); + if (tz->comment) + g_free(tz->comment); + + g_free(tz); +} +#endif + + +static int +compare_country_names (const void *a, const void *b) +{ + const TzLocation *tza = * (TzLocation **) a; + const TzLocation *tzb = * (TzLocation **) b; + + return strcmp (tza->zone, tzb->zone); +} + + +static void +sort_locations_by_country (GPtrArray *locations) +{ + qsort (locations->pdata, locations->len, sizeof (gpointer), + compare_country_names); +} + +static void +load_backward_tz (TzDB *tz_db) +{ + GError *error = NULL; + char **lines, *contents; + guint i; + + tz_db->backward = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + if (g_file_get_contents (GNOMECC_DATA_DIR "/datetime/backward", &contents, NULL, &error) == FALSE) + { + g_warning ("Failed to load 'backward' file: %s", error->message); + return; + } + lines = g_strsplit (contents, "\n", -1); + g_free (contents); + for (i = 0; lines[i] != NULL; i++) + { + char **items; + guint j; + char *real, *alias; + + if (g_ascii_strncasecmp (lines[i], "Link\t", 5) != 0) + continue; + + items = g_strsplit (lines[i], "\t", -1); + real = NULL; + alias = NULL; + /* Skip the "Link<tab>" part */ + for (j = 1; items[j] != NULL; j++) + { + if (items[j][0] == '\0') + continue; + if (real == NULL) + { + real = items[j]; + continue; + } + alias = items[j]; + break; + } + + if (real == NULL || alias == NULL) + g_warning ("Could not parse line: %s", lines[i]); + + /* We don't need more than one name for it */ + if (g_str_equal (real, "Etc/UTC") || + g_str_equal (real, "Etc/UCT")) + real = "Etc/GMT"; + + g_hash_table_insert (tz_db->backward, g_strdup (alias), g_strdup (real)); + g_strfreev (items); + } + g_strfreev (lines); +} + diff --git a/plugins/datetime/tz.h b/plugins/datetime/tz.h new file mode 100644 index 0000000..ab5535c --- /dev/null +++ b/plugins/datetime/tz.h @@ -0,0 +1,89 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* Generic timezone utilities. + * + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Authors: Hans Petter Jansson <hpj@ximian.com> + * + * Largely based on Michael Fulbright's work on Anaconda. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + + +#ifndef _E_TZ_H +#define _E_TZ_H + +#include <glib.h> + +#ifndef __sun +# define TZ_DATA_FILE "/usr/share/zoneinfo/zone.tab" +#else +# define TZ_DATA_FILE "/usr/share/lib/zoneinfo/tab/zone_sun.tab" +#endif + +typedef struct _TzDB TzDB; +typedef struct _TzLocation TzLocation; +typedef struct _TzInfo TzInfo; + + +struct _TzDB +{ + GPtrArray *locations; + GHashTable *backward; +}; + +struct _TzLocation +{ + gchar *country; + gdouble latitude; + gdouble longitude; + gchar *zone; + gchar *comment; + + gdouble dist; /* distance to clicked point for comparison */ +}; + +/* see the glibc info page information on time zone information */ +/* tzname_normal is the default name for the timezone */ +/* tzname_daylight is the name of the zone when in daylight savings */ +/* utc_offset is offset in seconds from utc */ +/* daylight if non-zero then location obeys daylight savings */ + +struct _TzInfo +{ + gchar *tzname_normal; + gchar *tzname_daylight; + glong utc_offset; + gint daylight; +}; + + +TzDB *tz_load_db (void); +void tz_db_free (TzDB *db); +char * tz_info_get_clean_name (TzDB *tz_db, + const char *tz); +GPtrArray *tz_get_locations (TzDB *db); +void tz_location_get_position (TzLocation *loc, + double *longitude, double *latitude); +void tz_location_free (TzLocation *loc); +char *tz_location_get_country (TzLocation *loc); +gchar *tz_location_get_zone (TzLocation *loc); +gchar *tz_location_get_comment (TzLocation *loc); +glong tz_location_get_utc_offset (TzLocation *loc); +gint tz_location_set_locally (TzLocation *loc); +TzInfo *tz_info_from_location (TzLocation *loc); +void tz_info_free (TzInfo *tz_info); + +#endif diff --git a/plugins/datetime/weather-tz.c b/plugins/datetime/weather-tz.c new file mode 100644 index 0000000..f1de2b9 --- /dev/null +++ b/plugins/datetime/weather-tz.c @@ -0,0 +1,122 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Kalev Lember <kalevlember@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include "weather-tz.h" +#include "tz.h" + +#define GWEATHER_I_KNOW_THIS_IS_UNSTABLE +#include <libgweather/gweather.h> + +static GList * +location_get_cities (GWeatherLocation *parent_location) +{ + GList *cities = NULL; + GWeatherLocation **children; + gint i; + + children = gweather_location_get_children (parent_location); + for (i = 0; children[i]; i++) { + if (gweather_location_get_level (children[i]) == GWEATHER_LOCATION_CITY) { + cities = g_list_prepend (cities, + children[i]); + } else { + cities = g_list_concat (cities, + location_get_cities (children[i])); + } + } + + return cities; +} + +static gboolean +weather_location_has_timezone (GWeatherLocation *loc) +{ + return gweather_location_get_timezone (loc) != NULL; +} + +/** + * load_timezones: + * @cities: a list of #GWeatherLocation + * + * Returns: a list of #TzLocation + */ +static GList * +load_timezones (GList *cities) +{ + GList *l; + GList *tz_locations = NULL; + + for (l = cities; l; l = l->next) { + TzLocation *loc; + const gchar *country; + const gchar *timezone_id; + gdouble latitude; + gdouble longitude; + + if (!gweather_location_has_coords (l->data) || + !weather_location_has_timezone (l->data)) { + g_debug ("Incomplete GWeather location entry: (%s) %s", + gweather_location_get_country (l->data), + gweather_location_get_city_name (l->data)); + continue; + } + + country = gweather_location_get_country (l->data); + timezone_id = gweather_timezone_get_tzid (gweather_location_get_timezone (l->data)); + gweather_location_get_coords (l->data, + &latitude, + &longitude); + + loc = g_new0 (TzLocation, 1); + loc->country = g_strdup (country); + loc->latitude = latitude; + loc->longitude = longitude; + loc->zone = g_strdup (timezone_id); + loc->comment = NULL; + + tz_locations = g_list_prepend (tz_locations, loc); + } + + return tz_locations; +} + +GList * +weather_tz_db_get_locations (const gchar *country_code) +{ + GList *cities; + GList *tz_locations; + GWeatherLocation *world; + GWeatherLocation *country; + + world = gweather_location_get_world (); + + country = gweather_location_find_by_country_code (world, country_code); + + if (!country) + return NULL; + + cities = location_get_cities (country); + tz_locations = load_timezones (cities); + + g_list_free (cities); + + return tz_locations; +} diff --git a/plugins/datetime/weather-tz.h b/plugins/datetime/weather-tz.h new file mode 100644 index 0000000..15b1571 --- /dev/null +++ b/plugins/datetime/weather-tz.h @@ -0,0 +1,27 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Kalev Lember <kalevlember@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __WEATHER_TZ_H +#define __WEATHER_TZ_H + +#include <glib.h> + +GList *weather_tz_db_get_locations (const char *country); + +#endif /* __WEATHER_TZ_H */ diff --git a/plugins/gsd.service.in b/plugins/gsd.service.in new file mode 100644 index 0000000..79b5f55 --- /dev/null +++ b/plugins/gsd.service.in @@ -0,0 +1,26 @@ +[Unit] +Description=@description@ service +CollectMode=inactive-or-failed +RefuseManualStart=true +RefuseManualStop=true + +After=gnome-session-initialized.target + +# Requisite/PartOf means the dependency is not loaded automatically. +# The ordering here also implies Before=gnome-session.target +Requisite=@plugin_dbus_name@.target +PartOf=@plugin_dbus_name@.target +Before=@plugin_dbus_name@.target + +@plugin_gate_units_section@ + +[Service] +Slice=session.slice +Type=dbus +ExecStart=@libexecdir@/gsd-@plugin_name@ +Restart=@plugin_restart@ +BusName=@plugin_dbus_name@ +TimeoutStopSec=5 +# We cannot use OnFailure as e.g. dependency failures are normal +# https://github.com/systemd/systemd/issues/12352 +ExecStopPost=@libexecdir@/gnome-session-ctl --exec-stop-check diff --git a/plugins/gsd.target.in b/plugins/gsd.target.in new file mode 100644 index 0000000..137df20 --- /dev/null +++ b/plugins/gsd.target.in @@ -0,0 +1,12 @@ +[Unit] +Description=@description@ target +CollectMode=inactive-or-failed + +# Pull in the service +Wants=@plugin_dbus_name@.service + +# Require GNOME session and specify startup ordering +Requisite=gnome-session-initialized.target +After=gnome-session-initialized.target +PartOf=gnome-session-initialized.target +Before=gnome-session.target diff --git a/plugins/housekeeping/gsd-disk-space-helper.c b/plugins/housekeeping/gsd-disk-space-helper.c new file mode 100644 index 0000000..56e054c --- /dev/null +++ b/plugins/housekeeping/gsd-disk-space-helper.c @@ -0,0 +1,159 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * vim: set et sw=8 ts=8: + * + * Copyright (c) 2008, Novell, Inc. + * Copyright (c) 2012, Red Hat, Inc. + * + * Authors: Vincent Untz <vuntz@gnome.org> + * Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <glib.h> +#include <gio/gio.h> + +#include "gsd-disk-space-helper.h" + +gboolean +gsd_should_ignore_unix_mount (GUnixMountEntry *mount) +{ + const char *fs, *device; + g_autofree char *label = NULL; + guint i; + + /* This is borrowed from GLib and used as a way to determine + * which mounts we should ignore by default. GLib doesn't + * expose this in a way that allows it to be used for this + * purpose + */ + + /* We also ignore network filesystems */ +#if !GLIB_CHECK_VERSION(2, 56, 0) + const gchar *ignore_fs[] = { + "adfs", + "afs", + "auto", + "autofs", + "autofs4", + "cgroup", + "configfs", + "cxfs", + "debugfs", + "devfs", + "devpts", + "devtmpfs", + "ecryptfs", + "fdescfs", + "fusectl", + "gfs", + "gfs2", + "gpfs", + "hugetlbfs", + "kernfs", + "linprocfs", + "linsysfs", + "lustre", + "lustre_lite", + "mfs", + "mqueue", + "ncpfs", + "nfsd", + "nullfs", + "ocfs2", + "overlay", + "proc", + "procfs", + "pstore", + "ptyfs", + "rootfs", + "rpc_pipefs", + "securityfs", + "selinuxfs", + "sysfs", + "tmpfs", + "usbfs", + "zfs", + NULL + }; +#endif /* GLIB < 2.56.0 */ + const gchar *ignore_network_fs[] = { + "cifs", + "nfs", + "nfs4", + "smbfs", + NULL + }; + const gchar *ignore_devices[] = { + "none", + "sunrpc", + "devpts", + "nfsd", + "/dev/loop", + "/dev/vn", + NULL + }; + const gchar *ignore_labels[] = { + "RETRODE", + NULL + }; + + fs = g_unix_mount_get_fs_type (mount); +#if GLIB_CHECK_VERSION(2, 56, 0) + if (g_unix_is_system_fs_type (fs)) + return TRUE; +#else + for (i = 0; ignore_fs[i] != NULL; i++) + if (g_str_equal (ignore_fs[i], fs)) + return TRUE; +#endif + for (i = 0; ignore_network_fs[i] != NULL; i++) + if (g_str_equal (ignore_network_fs[i], fs)) + return TRUE; + + device = g_unix_mount_get_device_path (mount); + for (i = 0; ignore_devices[i] != NULL; i++) + if (g_str_equal (ignore_devices[i], device)) + return TRUE; + + label = g_unix_mount_guess_name (mount); + for (i = 0; ignore_labels[i] != NULL; i++) + if (g_str_equal (ignore_labels[i], label)) + return TRUE; + + return FALSE; +} + +/* Used in gnome-control-center's info panel */ +gboolean +gsd_is_removable_mount (GUnixMountEntry *mount) +{ + const char *mount_path; + char *path; + + mount_path = g_unix_mount_get_mount_path (mount); + if (mount_path == NULL) + return FALSE; + + path = g_strdup_printf ("/run/media/%s", g_get_user_name ()); + if (g_str_has_prefix (mount_path, path)) { + g_free (path); + return TRUE; + } + g_free (path); + return FALSE; +} diff --git a/plugins/housekeeping/gsd-disk-space-helper.h b/plugins/housekeeping/gsd-disk-space-helper.h new file mode 100644 index 0000000..3898359 --- /dev/null +++ b/plugins/housekeeping/gsd-disk-space-helper.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * vim: set et sw=8 ts=8: + * + * Copyright (c) 2008, Novell, Inc. + * Copyright (c) 2012, Red Hat, Inc. + * + * Authors: Vincent Untz <vuntz@gnome.org> + * Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_DISK_SPACE_HELPER_H +#define __GSD_DISK_SPACE_HELPER_H + +#include <glib.h> +#include <gio/gunixmounts.h> + +G_BEGIN_DECLS + +gboolean gsd_should_ignore_unix_mount (GUnixMountEntry *mount); +gboolean gsd_is_removable_mount (GUnixMountEntry *mount); + +G_END_DECLS + +#endif /* __GSD_DISK_SPACE_HELPER_H */ diff --git a/plugins/housekeeping/gsd-disk-space-test.c b/plugins/housekeeping/gsd-disk-space-test.c new file mode 100644 index 0000000..c0410f8 --- /dev/null +++ b/plugins/housekeeping/gsd-disk-space-test.c @@ -0,0 +1,48 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * vim: set et sw=8 ts=8: + * + * Copyright (c) 2008, Novell, Inc. + * + * Authors: Vincent Untz <vuntz@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" +#include <gtk/gtk.h> +#include <libnotify/notify.h> +#include "gsd-disk-space.h" + +int +main (int argc, + char **argv) +{ + GMainLoop *loop; + + gtk_init (&argc, &argv); + notify_init ("gsd-disk-space-test"); + + loop = g_main_loop_new (NULL, FALSE); + + gsd_ldsm_setup (TRUE); + + g_main_loop_run (loop); + + gsd_ldsm_clean (); + g_main_loop_unref (loop); + + return 0; +} + diff --git a/plugins/housekeeping/gsd-disk-space.c b/plugins/housekeeping/gsd-disk-space.c new file mode 100644 index 0000000..9d57531 --- /dev/null +++ b/plugins/housekeeping/gsd-disk-space.c @@ -0,0 +1,1082 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * vim: set et sw=8 ts=8: + * + * Copyright (c) 2008, Novell, Inc. + * + * Authors: Vincent Untz <vuntz@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <sys/statvfs.h> +#include <time.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <glib-object.h> +#include <gio/gunixmounts.h> +#include <gio/gio.h> +#include <gtk/gtk.h> +#include <libnotify/notify.h> + +#include "gsd-disk-space.h" +#include "gsd-disk-space-helper.h" + +#define GIGABYTE 1024 * 1024 * 1024 + +#define CHECK_EVERY_X_SECONDS 60 + +#define DISK_SPACE_ANALYZER "baobab" + +#define SETTINGS_HOUSEKEEPING_DIR "org.gnome.settings-daemon.plugins.housekeeping" +#define SETTINGS_FREE_PC_NOTIFY_KEY "free-percent-notify" +#define SETTINGS_FREE_PC_NOTIFY_AGAIN_KEY "free-percent-notify-again" +#define SETTINGS_FREE_SIZE_NO_NOTIFY "free-size-gb-no-notify" +#define SETTINGS_MIN_NOTIFY_PERIOD "min-notify-period" +#define SETTINGS_IGNORE_PATHS "ignore-paths" + +#define PRIVACY_SETTINGS "org.gnome.desktop.privacy" +#define SETTINGS_PURGE_TRASH "remove-old-trash-files" +#define SETTINGS_PURGE_TEMP_FILES "remove-old-temp-files" +#define SETTINGS_PURGE_AFTER "old-files-age" + +typedef struct +{ + GUnixMountEntry *mount; + struct statvfs buf; + time_t notify_time; +} LdsmMountInfo; + +static GHashTable *ldsm_notified_hash = NULL; +static unsigned int ldsm_timeout_id = 0; +static GUnixMountMonitor *ldsm_monitor = NULL; +static double free_percent_notify = 0.05; +static double free_percent_notify_again = 0.01; +static unsigned int free_size_gb_no_notify = 2; +static unsigned int min_notify_period = 10; +static GSList *ignore_paths = NULL; +static GSettings *settings = NULL; +static GSettings *privacy_settings = NULL; +static NotifyNotification *notification = NULL; + +static guint64 *time_read; + +static gboolean purge_trash; +static gboolean purge_temp_files; +static guint purge_after; +static guint purge_trash_id = 0; +static guint purge_temp_id = 0; + +static gchar* +ldsm_get_fs_id_for_path (const gchar *path) +{ + GFile *file; + GFileInfo *fileinfo; + gchar *attr_id_fs; + + file = g_file_new_for_path (path); + fileinfo = g_file_query_info (file, G_FILE_ATTRIBUTE_ID_FILESYSTEM, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL); + if (fileinfo) { + attr_id_fs = g_strdup (g_file_info_get_attribute_string (fileinfo, G_FILE_ATTRIBUTE_ID_FILESYSTEM)); + g_object_unref (fileinfo); + } else { + attr_id_fs = NULL; + } + + g_object_unref (file); + + return attr_id_fs; +} + +static gboolean +ldsm_mount_has_trash (const char *path) +{ + const gchar *user_data_dir; + gchar *user_data_attr_id_fs; + gchar *path_attr_id_fs; + gboolean mount_uses_user_trash = FALSE; + gchar *trash_files_dir; + gboolean has_trash = FALSE; + GDir *dir; + + user_data_dir = g_get_user_data_dir (); + user_data_attr_id_fs = ldsm_get_fs_id_for_path (user_data_dir); + + path_attr_id_fs = ldsm_get_fs_id_for_path (path); + + if (g_strcmp0 (user_data_attr_id_fs, path_attr_id_fs) == 0) { + /* The volume that is low on space is on the same volume as our home + * directory. This means the trash is at $XDG_DATA_HOME/Trash, + * not at the root of the volume which is full. + */ + mount_uses_user_trash = TRUE; + } + + g_free (user_data_attr_id_fs); + g_free (path_attr_id_fs); + + /* I can't think of a better way to find out if a volume has any trash. Any suggestions? */ + if (mount_uses_user_trash) { + trash_files_dir = g_build_filename (g_get_user_data_dir (), "Trash", "files", NULL); + } else { + gchar *uid; + + uid = g_strdup_printf ("%d", getuid ()); + trash_files_dir = g_build_filename (path, ".Trash", uid, "files", NULL); + if (!g_file_test (trash_files_dir, G_FILE_TEST_IS_DIR)) { + gchar *trash_dir; + + g_free (trash_files_dir); + trash_dir = g_strdup_printf (".Trash-%s", uid); + trash_files_dir = g_build_filename (path, trash_dir, "files", NULL); + g_free (trash_dir); + if (!g_file_test (trash_files_dir, G_FILE_TEST_IS_DIR)) { + g_free (trash_files_dir); + g_free (uid); + return has_trash; + } + } + g_free (uid); + } + + dir = g_dir_open (trash_files_dir, 0, NULL); + if (dir) { + if (g_dir_read_name (dir)) + has_trash = TRUE; + g_dir_close (dir); + } + + g_free (trash_files_dir); + + return has_trash; +} + +static void +ldsm_analyze_path (const gchar *path) +{ + const gchar *argv[] = { DISK_SPACE_ANALYZER, path, NULL }; + + g_spawn_async (NULL, (gchar **) argv, NULL, G_SPAWN_SEARCH_PATH, + NULL, NULL, NULL, NULL); +} + +static void +ignore_callback (NotifyNotification *n, + const char *action) +{ + g_assert (action != NULL); + g_assert (strcmp (action, "ignore") == 0); + + /* Do nothing */ + + notify_notification_close (n, NULL); +} + +static void +examine_callback (NotifyNotification *n, + const char *action, + const char *path) +{ + g_assert (action != NULL); + g_assert (strcmp (action, "examine") == 0); + + ldsm_analyze_path (path); + + notify_notification_close (n, NULL); +} + +static gboolean +should_purge_file (GFile *file, + GCancellable *cancellable, + GDateTime *old) +{ + GFileInfo *info; + GDateTime *date; + gboolean should_purge; + + should_purge = FALSE; + + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_TRASH_DELETION_DATE "," + G_FILE_ATTRIBUTE_UNIX_UID "," + G_FILE_ATTRIBUTE_TIME_CHANGED, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable, + NULL); + if (!info) + return FALSE; + + date = g_file_info_get_deletion_date (info); + if (date == NULL) { + guint uid; + guint64 ctime; + + uid = g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_UID); + if (uid != getuid ()) { + should_purge = FALSE; + goto out; + } + + ctime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_CHANGED); + date = g_date_time_new_from_unix_local ((gint64) ctime); + } + + should_purge = g_date_time_difference (old, date) >= 0; + g_date_time_unref (date); + +out: + g_object_unref (info); + + return should_purge; +} + +DeleteData * +delete_data_new (GFile *file, + GCancellable *cancellable, + GDateTime *old, + gboolean dry_run, + gboolean trash, + gint depth, + const char *filesystem) +{ + DeleteData *data; + + data = g_new (DeleteData, 1); + data->ref_count = 1; + data->file = g_object_ref (file); + data->filesystem = g_strdup (filesystem); + data->cancellable = cancellable ? g_object_ref (cancellable) : NULL; + data->old = g_date_time_ref (old); + data->dry_run = dry_run; + data->trash = trash; + data->depth = depth; + data->name = g_file_get_parse_name (data->file); + + return data; +} + +char* +get_filesystem (GFile *file) +{ + g_autoptr(GFileInfo) info = NULL; + + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_ID_FILESYSTEM, + G_FILE_QUERY_INFO_NONE, + NULL, + NULL); + + return g_file_info_get_attribute_as_string (info, + G_FILE_ATTRIBUTE_ID_FILESYSTEM); +} + +static DeleteData * +delete_data_ref (DeleteData *data) +{ + data->ref_count += 1; + return data; +} + +void +delete_data_unref (DeleteData *data) +{ + data->ref_count -= 1; + if (data->ref_count > 0) + return; + + g_object_unref (data->file); + if (data->cancellable) + g_object_unref (data->cancellable); + g_date_time_unref (data->old); + g_free (data->name); + g_free (data->filesystem); + g_free (data); +} + +static void +delete_batch (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GFileEnumerator *enumerator = G_FILE_ENUMERATOR (source); + DeleteData *data = user_data; + const char *fs; + GList *files, *f; + GFile *child_file; + DeleteData *child; + GFileInfo *info; + GError *error = NULL; + + files = g_file_enumerator_next_files_finish (enumerator, res, &error); + + g_debug ("GsdHousekeeping: purging %d children of %s", g_list_length (files), data->name); + + if (files) { + for (f = files; f; f = f->next) { + if (g_cancellable_is_cancelled (data->cancellable)) + break; + info = f->data; + + fs = g_file_info_get_attribute_string (info, + G_FILE_ATTRIBUTE_ID_FILESYSTEM); + + /* Do not consider the file if it is on a different file system. + * Ignore data->filesystem if it is NULL (this only happens if + * it is the toplevel trash directory). */ + if (data->filesystem && g_strcmp0 (fs, data->filesystem) != 0) { + g_debug ("GsdHousekeeping: skipping file \"%s\" as it is on a different file system", + g_file_info_get_name (info)); + continue; + } + + child_file = g_file_get_child (data->file, g_file_info_get_name (info)); + child = delete_data_new (child_file, + data->cancellable, + data->old, + data->dry_run, + data->trash, + data->depth + 1, + fs); + delete_recursively_by_age (child); + delete_data_unref (child); + g_object_unref (child_file); + } + g_list_free_full (files, g_object_unref); + if (!g_cancellable_is_cancelled (data->cancellable)) { + g_file_enumerator_next_files_async (enumerator, 20, + 0, + data->cancellable, + delete_batch, + data); + return; + } + } + + g_file_enumerator_close (enumerator, data->cancellable, NULL); + g_object_unref (enumerator); + + if (data->depth > 0 && !g_cancellable_is_cancelled (data->cancellable)) { + if ((data->trash && data->depth > 1) || + should_purge_file (data->file, data->cancellable, data->old)) { + g_debug ("GsdHousekeeping: purging %s\n", data->name); + if (!data->dry_run) { + g_file_delete (data->file, data->cancellable, NULL); + } + } + } + delete_data_unref (data); +} + +static void +delete_subdir (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GFile *file = G_FILE (source); + DeleteData *data = user_data; + GFileEnumerator *enumerator; + GError *error = NULL; + + g_debug ("GsdHousekeeping: purging %s in %s\n", + data->trash ? "trash" : "temporary files", data->name); + + enumerator = g_file_enumerate_children_finish (file, res, &error); + if (error) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY) && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED)) + g_warning ("Failed to enumerate children of %s: %s\n", data->name, error->message); + } + if (enumerator) { + g_file_enumerator_next_files_async (enumerator, 20, + 0, + data->cancellable, + delete_batch, + delete_data_ref (data)); + } else if (data->depth > 0 && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY)) { + if ((data->trash && data->depth > 1) || + should_purge_file (data->file, data->cancellable, data->old)) { + g_debug ("Purging %s leaf node", data->name); + if (!data->dry_run) { + g_file_delete (data->file, data->cancellable, NULL); + } + } + } + + if (error) + g_error_free (error); + delete_data_unref (data); +} + +static void +delete_subdir_check_symlink (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GFile *file = G_FILE (source); + DeleteData *data = user_data; + GFileInfo *info; + + info = g_file_query_info_finish (file, res, NULL); + if (!info) { + delete_data_unref (data); + return; + } + + if (g_file_info_get_file_type (info) == G_FILE_TYPE_SYMBOLIC_LINK) { + if (should_purge_file (data->file, data->cancellable, data->old)) { + g_debug ("Purging %s leaf node", data->name); + if (!data->dry_run) { + g_file_delete (data->file, data->cancellable, NULL); + } + } + } else if (g_strcmp0 (g_file_info_get_name (info), ".X11-unix") == 0) { + g_debug ("Skipping X11 socket directory"); + } else { + g_file_enumerate_children_async (data->file, + G_FILE_ATTRIBUTE_STANDARD_NAME "," + G_FILE_ATTRIBUTE_STANDARD_TYPE "," + G_FILE_ATTRIBUTE_ID_FILESYSTEM, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + 0, + data->cancellable, + delete_subdir, + delete_data_ref (data)); + } + g_object_unref (info); + delete_data_unref (data); +} + +void +delete_recursively_by_age (DeleteData *data) +{ + if (data->trash && (data->depth == 1) && + !should_purge_file (data->file, data->cancellable, data->old)) { + /* no need to recurse into trashed directories */ + return; + } + + g_file_query_info_async (data->file, + G_FILE_ATTRIBUTE_STANDARD_NAME "," + G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + 0, + data->cancellable, + delete_subdir_check_symlink, + delete_data_ref (data)); +} + +void +gsd_ldsm_purge_trash (GDateTime *old) +{ + GFile *file; + DeleteData *data; + + file = g_file_new_for_uri ("trash:"); + data = delete_data_new (file, NULL, old, FALSE, TRUE, 0, NULL); + delete_recursively_by_age (data); + delete_data_unref (data); + g_object_unref (file); +} + +void +gsd_ldsm_purge_temp_files (GDateTime *old) +{ + DeleteData *data; + GFile *file; + char *filesystem; + + /* Never clean temporary files on a sane (i.e. systemd managed) + * system. In that case systemd already ships + * /usr/lib/tmpfiles.d/tmp.conf + * which does the trick in a much safer way. + * Ideally we can just drop this feature, I am not sure why it was + * added in the first place though, it does not really seem like a + * privacy feature (also, it was late in the release cycle). + * https://en.wikipedia.org/wiki/Wikipedia:Chesterton%27s_fence + * + * This does the same as sd_booted without needing libsystemd. + */ + if (g_file_test ("/run/systemd/system/", G_FILE_TEST_IS_DIR)) + return; + + file = g_file_new_for_path (g_get_tmp_dir ()); + filesystem = get_filesystem (file); + data = delete_data_new (file, NULL, old, FALSE, FALSE, 0, filesystem); + g_free (filesystem); + delete_recursively_by_age (data); + delete_data_unref (data); + g_object_unref (file); + + if (g_strcmp0 (g_get_tmp_dir (), "/var/tmp") != 0) { + file = g_file_new_for_path ("/var/tmp"); + filesystem = get_filesystem (file); + data = delete_data_new (file, NULL, old, FALSE, FALSE, 0, filesystem); + g_free (filesystem); + delete_recursively_by_age (data); + delete_data_unref (data); + g_object_unref (file); + } + + if (g_strcmp0 (g_get_tmp_dir (), "/tmp") != 0) { + file = g_file_new_for_path ("/tmp"); + filesystem = get_filesystem (file); + data = delete_data_new (file, NULL, old, FALSE, FALSE, 0, filesystem); + g_free (filesystem); + delete_recursively_by_age (data); + delete_data_unref (data); + g_object_unref (file); + } +} + +void +gsd_ldsm_show_empty_trash (void) +{ + GFile *file; + GDateTime *old; + DeleteData *data; + + old = g_date_time_new_now_local (); + file = g_file_new_for_uri ("trash:"); + data = delete_data_new (file, NULL, old, TRUE, TRUE, 0, NULL); + g_object_unref (file); + g_date_time_unref (old); + + delete_recursively_by_age (data); + delete_data_unref (data); +} + +static gboolean +ldsm_purge_trash_and_temp (gpointer data) +{ + GDateTime *now, *old; + + now = g_date_time_new_now_local (); + old = g_date_time_add_days (now, - purge_after); + + if (purge_trash) { + g_debug ("housekeeping: purge trash older than %u days", purge_after); + gsd_ldsm_purge_trash (old); + } + if (purge_temp_files) { + g_debug ("housekeeping: purge temp files older than %u days", purge_after); + gsd_ldsm_purge_temp_files (old); + } + + g_date_time_unref (old); + g_date_time_unref (now); + + return G_SOURCE_CONTINUE; +} + +static void +empty_trash_callback (NotifyNotification *n, + const char *action) +{ + GDateTime *old; + + g_assert (action != NULL); + g_assert (strcmp (action, "empty-trash") == 0); + + old = g_date_time_new_now_local (); + gsd_ldsm_purge_trash (old); + g_date_time_unref (old); + + notify_notification_close (n, NULL); +} + +static void +on_notification_closed (NotifyNotification *n) +{ + g_object_unref (notification); + notification = NULL; +} + +static void +ldsm_notify (const char *summary, + const char *body, + const char *mount_path) +{ + gchar *program; + gboolean has_disk_analyzer; + gboolean has_trash; + + /* Don't show a notice if one is already displayed */ + if (notification != NULL) + return; + + notification = notify_notification_new (summary, body, "drive-harddisk-symbolic"); + g_signal_connect (notification, + "closed", + G_CALLBACK (on_notification_closed), + NULL); + + notify_notification_set_app_name (notification, _("Disk Space")); + notify_notification_set_hint (notification, "transient", g_variant_new_boolean (TRUE)); + notify_notification_set_urgency (notification, NOTIFY_URGENCY_CRITICAL); + notify_notification_set_timeout (notification, NOTIFY_EXPIRES_DEFAULT); + notify_notification_set_hint_string (notification, "desktop-entry", "org.gnome.baobab"); + + program = g_find_program_in_path (DISK_SPACE_ANALYZER); + has_disk_analyzer = (program != NULL); + g_free (program); + + if (has_disk_analyzer) { + notify_notification_add_action (notification, + "examine", + _("Examine"), + (NotifyActionCallback) examine_callback, + g_strdup (mount_path), + g_free); + } + + has_trash = ldsm_mount_has_trash (mount_path); + + if (has_trash) { + notify_notification_add_action (notification, + "empty-trash", + _("Empty Trash"), + (NotifyActionCallback) empty_trash_callback, + NULL, + NULL); + } + + notify_notification_add_action (notification, + "ignore", + _("Ignore"), + (NotifyActionCallback) ignore_callback, + NULL, + NULL); + notify_notification_set_category (notification, "device"); + + if (!notify_notification_show (notification, NULL)) { + g_warning ("failed to send disk space notification\n"); + } +} + +static void +ldsm_notify_for_mount (LdsmMountInfo *mount, + gboolean multiple_volumes) +{ + gboolean has_trash; + gchar *name; + gint64 free_space; + const gchar *path; + char *free_space_str; + char *summary; + char *body; + + name = g_unix_mount_guess_name (mount->mount); + path = g_unix_mount_get_mount_path (mount->mount); + has_trash = ldsm_mount_has_trash (path); + + free_space = (gint64) mount->buf.f_frsize * (gint64) mount->buf.f_bavail; + free_space_str = g_format_size (free_space); + + if (multiple_volumes) { + summary = g_strdup_printf (_("Low Disk Space on “%s”"), name); + if (has_trash) { + body = g_strdup_printf (_("The volume “%s” has only %s disk space remaining. You may free up some space by emptying the trash."), + name, + free_space_str); + } else { + body = g_strdup_printf (_("The volume “%s” has only %s disk space remaining."), + name, + free_space_str); + } + } else { + summary = g_strdup (_("Low Disk Space")); + if (has_trash) { + body = g_strdup_printf (_("This computer has only %s disk space remaining. You may free up some space by emptying the trash."), + free_space_str); + } else { + body = g_strdup_printf (_("This computer has only %s disk space remaining."), + free_space_str); + } + } + + ldsm_notify (summary, body, path); + + g_free (free_space_str); + g_free (summary); + g_free (body); + g_free (name); +} + +static gboolean +ldsm_mount_has_space (LdsmMountInfo *mount) +{ + gdouble free_space; + + free_space = (double) mount->buf.f_bavail / (double) mount->buf.f_blocks; + /* enough free space, nothing to do */ + if (free_space > free_percent_notify) + return TRUE; + + if (((gint64) mount->buf.f_frsize * (gint64) mount->buf.f_bavail) > ((gint64) free_size_gb_no_notify * GIGABYTE)) + return TRUE; + + /* If we got here, then this volume is low on space */ + return FALSE; +} + +static gboolean +ldsm_mount_is_virtual (LdsmMountInfo *mount) +{ + if (mount->buf.f_blocks == 0) { + /* Filesystems with zero blocks are virtual */ + return TRUE; + } + + return FALSE; +} + +static gint +ldsm_ignore_path_compare (gconstpointer a, + gconstpointer b) +{ + return g_strcmp0 ((const gchar *)a, (const gchar *)b); +} + +static gboolean +ldsm_mount_is_user_ignore (const gchar *path) +{ + if (g_slist_find_custom (ignore_paths, path, (GCompareFunc) ldsm_ignore_path_compare) != NULL) + return TRUE; + else + return FALSE; +} + + +static void +ldsm_free_mount_info (gpointer data) +{ + LdsmMountInfo *mount = data; + + g_return_if_fail (mount != NULL); + + g_unix_mount_free (mount->mount); + g_free (mount); +} + +static void +ldsm_maybe_warn_mounts (GList *mounts, + gboolean multiple_volumes) +{ + GList *l; + gboolean done = FALSE; + + for (l = mounts; l != NULL; l = l->next) { + LdsmMountInfo *mount_info = l->data; + LdsmMountInfo *previous_mount_info; + gdouble free_space; + gdouble previous_free_space; + time_t curr_time; + const gchar *path; + gboolean show_notify; + + if (done) { + /* Don't show any more dialogs if the user took action with the last one. The user action + * might free up space on multiple volumes, making the next dialog redundant. + */ + ldsm_free_mount_info (mount_info); + continue; + } + + path = g_unix_mount_get_mount_path (mount_info->mount); + + previous_mount_info = g_hash_table_lookup (ldsm_notified_hash, path); + if (previous_mount_info != NULL) + previous_free_space = (gdouble) previous_mount_info->buf.f_bavail / (gdouble) previous_mount_info->buf.f_blocks; + + free_space = (gdouble) mount_info->buf.f_bavail / (gdouble) mount_info->buf.f_blocks; + + if (previous_mount_info == NULL) { + /* We haven't notified for this mount yet */ + show_notify = TRUE; + mount_info->notify_time = time (NULL); + g_hash_table_replace (ldsm_notified_hash, g_strdup (path), mount_info); + } else if ((previous_free_space - free_space) > free_percent_notify_again) { + /* We've notified for this mount before and free space has decreased sufficiently since last time to notify again */ + curr_time = time (NULL); + if (difftime (curr_time, previous_mount_info->notify_time) > (gdouble)(min_notify_period * 60)) { + show_notify = TRUE; + mount_info->notify_time = curr_time; + } else { + /* It's too soon to show the dialog again. However, we still replace the LdsmMountInfo + * struct in the hash table, but give it the notfiy time from the previous dialog. + * This will stop the notification from reappearing unnecessarily as soon as the timeout expires. + */ + show_notify = FALSE; + mount_info->notify_time = previous_mount_info->notify_time; + } + g_hash_table_replace (ldsm_notified_hash, g_strdup (path), mount_info); + } else { + /* We've notified for this mount before, but the free space hasn't decreased sufficiently to notify again */ + ldsm_free_mount_info (mount_info); + show_notify = FALSE; + } + + if (show_notify) { + ldsm_notify_for_mount (mount_info, multiple_volumes); + done = TRUE; + } + } +} + +static gboolean +ldsm_check_all_mounts (gpointer data) +{ + GList *mounts; + GList *l; + GList *check_mounts = NULL; + GList *full_mounts = NULL; + guint number_of_mounts = 0; + gboolean multiple_volumes = FALSE; + + /* We iterate through the static mounts in /etc/fstab first, seeing if + * they're mounted by checking if the GUnixMountPoint has a corresponding GUnixMountEntry. + * Iterating through the static mounts means we automatically ignore dynamically mounted media. + */ + mounts = g_unix_mount_points_get (time_read); + + for (l = mounts; l != NULL; l = l->next) { + GUnixMountPoint *mount_point = l->data; + GUnixMountEntry *mount; + LdsmMountInfo *mount_info; + const gchar *path; + + path = g_unix_mount_point_get_mount_path (mount_point); + mount = g_unix_mount_at (path, time_read); + g_unix_mount_point_free (mount_point); + if (mount == NULL) { + /* The GUnixMountPoint is not mounted */ + continue; + } + + mount_info = g_new0 (LdsmMountInfo, 1); + mount_info->mount = mount; + + path = g_unix_mount_get_mount_path (mount); + + if (g_unix_mount_is_readonly (mount)) { + ldsm_free_mount_info (mount_info); + continue; + } + + if (ldsm_mount_is_user_ignore (path)) { + ldsm_free_mount_info (mount_info); + continue; + } + + if (gsd_should_ignore_unix_mount (mount)) { + ldsm_free_mount_info (mount_info); + continue; + } + + if (statvfs (path, &mount_info->buf) != 0) { + ldsm_free_mount_info (mount_info); + continue; + } + + if (ldsm_mount_is_virtual (mount_info)) { + ldsm_free_mount_info (mount_info); + continue; + } + + check_mounts = g_list_prepend (check_mounts, mount_info); + number_of_mounts += 1; + } + + g_list_free (mounts); + + if (number_of_mounts > 1) + multiple_volumes = TRUE; + + for (l = check_mounts; l != NULL; l = l->next) { + LdsmMountInfo *mount_info = l->data; + + if (!ldsm_mount_has_space (mount_info)) { + full_mounts = g_list_prepend (full_mounts, mount_info); + } else { + g_hash_table_remove (ldsm_notified_hash, g_unix_mount_get_mount_path (mount_info->mount)); + ldsm_free_mount_info (mount_info); + } + } + + ldsm_maybe_warn_mounts (full_mounts, multiple_volumes); + + g_list_free (check_mounts); + g_list_free (full_mounts); + + return TRUE; +} + +static gboolean +ldsm_is_hash_item_not_in_mounts (gpointer key, + gpointer value, + gpointer user_data) +{ + GList *l; + + for (l = (GList *) user_data; l != NULL; l = l->next) { + GUnixMountEntry *mount = l->data; + const char *path; + + path = g_unix_mount_get_mount_path (mount); + + if (strcmp (path, key) == 0) + return FALSE; + } + + return TRUE; +} + +static void +ldsm_mounts_changed (GObject *monitor, + gpointer data) +{ + GList *mounts; + + /* remove the saved data for mounts that got removed */ + mounts = g_unix_mounts_get (time_read); + g_hash_table_foreach_remove (ldsm_notified_hash, + ldsm_is_hash_item_not_in_mounts, mounts); + g_list_free_full (mounts, (GDestroyNotify) g_unix_mount_free); + + /* check the status now, for the new mounts */ + ldsm_check_all_mounts (NULL); + + /* and reset the timeout */ + if (ldsm_timeout_id) + g_source_remove (ldsm_timeout_id); + ldsm_timeout_id = g_timeout_add_seconds (CHECK_EVERY_X_SECONDS, + ldsm_check_all_mounts, NULL); + g_source_set_name_by_id (ldsm_timeout_id, "[gnome-settings-daemon] ldsm_check_all_mounts"); +} + +static gboolean +ldsm_is_hash_item_in_ignore_paths (gpointer key, + gpointer value, + gpointer user_data) +{ + return ldsm_mount_is_user_ignore (key); +} + +static void +gsd_ldsm_get_config (void) +{ + gchar **settings_list; + + free_percent_notify = g_settings_get_double (settings, SETTINGS_FREE_PC_NOTIFY_KEY); + free_percent_notify_again = g_settings_get_double (settings, SETTINGS_FREE_PC_NOTIFY_AGAIN_KEY); + + free_size_gb_no_notify = g_settings_get_int (settings, SETTINGS_FREE_SIZE_NO_NOTIFY); + min_notify_period = g_settings_get_int (settings, SETTINGS_MIN_NOTIFY_PERIOD); + + if (ignore_paths != NULL) { + g_slist_foreach (ignore_paths, (GFunc) g_free, NULL); + g_clear_pointer (&ignore_paths, g_slist_free); + } + + settings_list = g_settings_get_strv (settings, SETTINGS_IGNORE_PATHS); + if (settings_list != NULL) { + guint i; + + for (i = 0; settings_list[i] != NULL; i++) + ignore_paths = g_slist_prepend (ignore_paths, g_strdup (settings_list[i])); + + /* Make sure we dont leave stale entries in ldsm_notified_hash */ + g_hash_table_foreach_remove (ldsm_notified_hash, + ldsm_is_hash_item_in_ignore_paths, NULL); + + g_strfreev (settings_list); + } + + purge_trash = g_settings_get_boolean (privacy_settings, SETTINGS_PURGE_TRASH); + purge_temp_files = g_settings_get_boolean (privacy_settings, SETTINGS_PURGE_TEMP_FILES); + purge_after = g_settings_get_uint (privacy_settings, SETTINGS_PURGE_AFTER); +} + +static void +gsd_ldsm_update_config (GSettings *settings, + const gchar *key, + gpointer user_data) +{ + gsd_ldsm_get_config (); +} + +void +gsd_ldsm_setup (gboolean check_now) +{ + if (ldsm_notified_hash || ldsm_timeout_id || ldsm_monitor) { + g_warning ("Low disk space monitor already initialized."); + return; + } + + ldsm_notified_hash = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + ldsm_free_mount_info); + + settings = g_settings_new (SETTINGS_HOUSEKEEPING_DIR); + privacy_settings = g_settings_new (PRIVACY_SETTINGS); + gsd_ldsm_get_config (); + g_signal_connect (G_OBJECT (settings), "changed", + G_CALLBACK (gsd_ldsm_update_config), NULL); + + ldsm_monitor = g_unix_mount_monitor_get (); + g_signal_connect (ldsm_monitor, "mounts-changed", + G_CALLBACK (ldsm_mounts_changed), NULL); + + if (check_now) + ldsm_check_all_mounts (NULL); + + ldsm_timeout_id = g_timeout_add_seconds (CHECK_EVERY_X_SECONDS, + ldsm_check_all_mounts, NULL); + g_source_set_name_by_id (ldsm_timeout_id, "[gnome-settings-daemon] ldsm_check_all_mounts"); + + purge_trash_id = g_timeout_add_seconds (3600, ldsm_purge_trash_and_temp, NULL); + g_source_set_name_by_id (purge_trash_id, "[gnome-settings-daemon] ldsm_purge_trash_and_temp"); +} + +void +gsd_ldsm_clean (void) +{ + if (purge_trash_id) + g_source_remove (purge_trash_id); + purge_trash_id = 0; + + if (purge_temp_id) + g_source_remove (purge_temp_id); + purge_temp_id = 0; + + if (ldsm_timeout_id) + g_source_remove (ldsm_timeout_id); + ldsm_timeout_id = 0; + + g_clear_pointer (&ldsm_notified_hash, g_hash_table_destroy); + g_clear_object (&ldsm_monitor); + g_clear_object (&settings); + g_clear_object (&privacy_settings); + /* NotifyNotification::closed callback will drop reference */ + if (notification != NULL) + notify_notification_close (notification, NULL); + g_slist_free_full (ignore_paths, g_free); + ignore_paths = NULL; +} + diff --git a/plugins/housekeeping/gsd-disk-space.h b/plugins/housekeeping/gsd-disk-space.h new file mode 100644 index 0000000..38c2963 --- /dev/null +++ b/plugins/housekeeping/gsd-disk-space.h @@ -0,0 +1,64 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * vim: set et sw=8 ts=8: + * + * Copyright (c) 2008, Novell, Inc. + * + * Authors: Vincent Untz <vuntz@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_DISK_SPACE_H +#define __GSD_DISK_SPACE_H + +#include <glib.h> + +G_BEGIN_DECLS + +typedef struct { + gint ref_count; + GFile *file; + GCancellable *cancellable; + GDateTime *old; + gboolean dry_run; + gboolean trash; + gchar *name; + gchar *filesystem; + gint depth; +} DeleteData; + +void delete_data_unref (DeleteData *data); +DeleteData *delete_data_new (GFile *file, + GCancellable *cancellable, + GDateTime *old, + gboolean dry_run, + gboolean trash, + gint depth, + const char *filesystem); +char* get_filesystem (GFile *file); + +void delete_recursively_by_age (DeleteData *data); + +void gsd_ldsm_setup (gboolean check_now); +void gsd_ldsm_clean (void); + +/* for the test */ +void gsd_ldsm_show_empty_trash (void); +void gsd_ldsm_purge_trash (GDateTime *old); +void gsd_ldsm_purge_temp_files (GDateTime *old); + +G_END_DECLS + +#endif /* __GSD_DISK_SPACE_H */ diff --git a/plugins/housekeeping/gsd-empty-trash-test.c b/plugins/housekeeping/gsd-empty-trash-test.c new file mode 100644 index 0000000..6a2c37f --- /dev/null +++ b/plugins/housekeeping/gsd-empty-trash-test.c @@ -0,0 +1,44 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * vim: set et sw=8 ts=8: + * + * Copyright (c) 2011, Red Hat, Inc. + * + * Authors: Cosimo Cecchi <cosimoc@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" +#include <gtk/gtk.h> +#include "gsd-disk-space.h" + +int +main (int argc, + char **argv) +{ + GMainLoop *loop; + + gtk_init (&argc, &argv); + + loop = g_main_loop_new (NULL, FALSE); + + gsd_ldsm_show_empty_trash (); + g_main_loop_run (loop); + + g_main_loop_unref (loop); + + return 0; +} + diff --git a/plugins/housekeeping/gsd-housekeeping-manager.c b/plugins/housekeeping/gsd-housekeeping-manager.c new file mode 100644 index 0000000..8ad985c --- /dev/null +++ b/plugins/housekeeping/gsd-housekeeping-manager.c @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2008 Michael J. Chudobiak <mjc@avtechpulse.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <gio/gio.h> +#include <glib/gstdio.h> +#include <string.h> +#include <libnotify/notify.h> + +#include "gnome-settings-profile.h" +#include "gsd-housekeeping-manager.h" +#include "gsd-disk-space.h" + + +/* General */ +#define INTERVAL_ONCE_A_DAY 24*60*60 +#define INTERVAL_TWO_MINUTES 2*60 + +/* Thumbnail cleaner */ +#define THUMB_PREFIX "org.gnome.desktop.thumbnail-cache" + +#define THUMB_AGE_KEY "maximum-age" +#define THUMB_SIZE_KEY "maximum-size" + +#define GSD_HOUSEKEEPING_DBUS_PATH "/org/gnome/SettingsDaemon/Housekeeping" + +static const gchar introspection_xml[] = +"<node>" +" <interface name='org.gnome.SettingsDaemon.Housekeeping'>" +" <method name='EmptyTrash'/>" +" <method name='RemoveTempFiles'/>" +" </interface>" +"</node>"; + +struct _GsdHousekeepingManager { + GObject parent; + + GSettings *settings; + guint long_term_cb; + guint short_term_cb; + + GDBusNodeInfo *introspection_data; + GDBusConnection *connection; + GCancellable *bus_cancellable; + guint name_id; +}; + +static void gsd_housekeeping_manager_class_init (GsdHousekeepingManagerClass *klass); +static void gsd_housekeeping_manager_init (GsdHousekeepingManager *housekeeping_manager); + +G_DEFINE_TYPE (GsdHousekeepingManager, gsd_housekeeping_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + + +typedef struct { + glong now; + glong max_age; + goffset total_size; + goffset max_size; +} PurgeData; + + +typedef struct { + gint64 time; + char *path; + glong size; +} ThumbData; + + +static void +thumb_data_free (gpointer data) +{ + ThumbData *info = data; + + if (info) { + g_free (info->path); + g_free (info); + } +} + +static GList * +read_dir_for_purge (const char *path, GList *files) +{ + GFile *read_path; + GFileEnumerator *enum_dir; + int cannot_get_time = 0; + + read_path = g_file_new_for_path (path); + enum_dir = g_file_enumerate_children (read_path, + G_FILE_ATTRIBUTE_STANDARD_NAME "," + G_FILE_ATTRIBUTE_TIME_ACCESS "," + G_FILE_ATTRIBUTE_TIME_MODIFIED "," + G_FILE_ATTRIBUTE_STANDARD_SIZE, + G_FILE_QUERY_INFO_NONE, + NULL, + NULL); + + if (enum_dir != NULL) { + GFileInfo *info; + while ((info = g_file_enumerator_next_file (enum_dir, NULL, NULL)) != NULL) { + const char *name; + name = g_file_info_get_name (info); + + if (strlen (name) == 36 && strcmp (name + 32, ".png") == 0) { + ThumbData *td; + GFile *entry; + char *entry_path; + gint64 time; + + entry = g_file_get_child (read_path, name); + entry_path = g_file_get_path (entry); + g_object_unref (entry); + + // If atime is available, it should be no worse than mtime. + // - Even if the file system is mounted with noatime, the atime and + // mtime will be set to the same value on file creation. + // - Since the thumbnailer never edits thumbnails, and instead swaps + // in newly created temp files, atime will always be >= mtime. + + if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_TIME_ACCESS)) { + time = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_ACCESS); + } else if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_TIME_MODIFIED)) { + time = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED); + } else { + // Unlikely to get here, but be defensive + cannot_get_time += 1; + time = G_MAXINT64; + } + + td = g_new0 (ThumbData, 1); + td->path = entry_path; + td->time = time; + td->size = g_file_info_get_size (info); + + files = g_list_prepend (files, td); + } + g_object_unref (info); + } + g_object_unref (enum_dir); + + if (cannot_get_time > 0) { + g_warning ("Could not read atime or mtime on %d files in %s", cannot_get_time, path); + } + } + g_object_unref (read_path); + + return files; +} + +static void +purge_old_thumbnails (ThumbData *info, PurgeData *purge_data) +{ + if ((purge_data->now - info->time) > purge_data->max_age) { + g_unlink (info->path); + info->size = 0; + } else { + purge_data->total_size += info->size; + } +} + +static int +sort_file_time (ThumbData *file1, ThumbData *file2) +{ + return file1->time - file2->time; +} + +static char ** +get_thumbnail_dirs (void) +{ + GPtrArray *array; + char *path; + + array = g_ptr_array_new (); + + /* check new XDG cache */ + path = g_build_filename (g_get_user_cache_dir (), + "thumbnails", + "normal", + NULL); + g_ptr_array_add (array, path); + + path = g_build_filename (g_get_user_cache_dir (), + "thumbnails", + "large", + NULL); + g_ptr_array_add (array, path); + + path = g_build_filename (g_get_user_cache_dir (), + "thumbnails", + "fail", + "gnome-thumbnail-factory", + NULL); + g_ptr_array_add (array, path); + + /* cleanup obsolete locations too */ + path = g_build_filename (g_get_home_dir (), + ".thumbnails", + "normal", + NULL); + g_ptr_array_add (array, path); + + path = g_build_filename (g_get_home_dir (), + ".thumbnails", + "large", + NULL); + g_ptr_array_add (array, path); + + path = g_build_filename (g_get_home_dir (), + ".thumbnails", + "fail", + "gnome-thumbnail-factory", + NULL); + g_ptr_array_add (array, path); + + g_ptr_array_add (array, NULL); + + return (char **) g_ptr_array_free (array, FALSE); +} + +static void +purge_thumbnail_cache (GsdHousekeepingManager *manager) +{ + + char **paths; + GList *files; + PurgeData purge_data; + GTimeVal current_time; + guint i; + + g_debug ("housekeeping: checking thumbnail cache size and freshness"); + + purge_data.max_age = (glong) g_settings_get_int (manager->settings, THUMB_AGE_KEY) * 24 * 60 * 60; + purge_data.max_size = (goffset) g_settings_get_int (manager->settings, THUMB_SIZE_KEY) * 1024 * 1024; + + /* if both are set to -1, we don't need to read anything */ + if ((purge_data.max_age < 0) && (purge_data.max_size < 0)) + return; + + paths = get_thumbnail_dirs (); + files = NULL; + for (i = 0; paths[i] != NULL; i++) + files = read_dir_for_purge (paths[i], files); + g_strfreev (paths); + + g_get_current_time (¤t_time); + + purge_data.now = current_time.tv_sec; + purge_data.total_size = 0; + + if (purge_data.max_age >= 0) + g_list_foreach (files, (GFunc) purge_old_thumbnails, &purge_data); + + if ((purge_data.total_size > purge_data.max_size) && (purge_data.max_size >= 0)) { + GList *scan; + files = g_list_sort (files, (GCompareFunc) sort_file_time); + for (scan = files; scan && (purge_data.total_size > purge_data.max_size); scan = scan->next) { + ThumbData *info = scan->data; + g_unlink (info->path); + purge_data.total_size -= info->size; + } + } + + g_list_foreach (files, (GFunc) thumb_data_free, NULL); + g_list_free (files); +} + +static gboolean +do_cleanup (GsdHousekeepingManager *manager) +{ + purge_thumbnail_cache (manager); + return TRUE; +} + +static gboolean +do_cleanup_once (GsdHousekeepingManager *manager) +{ + do_cleanup (manager); + manager->short_term_cb = 0; + return FALSE; +} + +static void +do_cleanup_soon (GsdHousekeepingManager *manager) +{ + if (manager->short_term_cb == 0) { + g_debug ("housekeeping: will tidy up in 2 minutes"); + manager->short_term_cb = g_timeout_add_seconds (INTERVAL_TWO_MINUTES, + (GSourceFunc) do_cleanup_once, + manager); + g_source_set_name_by_id (manager->short_term_cb, "[gnome-settings-daemon] do_cleanup_once"); + } +} + +static void +settings_changed_callback (GSettings *settings, + const char *key, + GsdHousekeepingManager *manager) +{ + do_cleanup_soon (manager); +} + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GDateTime *now; + now = g_date_time_new_now_local (); + if (g_strcmp0 (method_name, "EmptyTrash") == 0) { + gsd_ldsm_purge_trash (now); + g_dbus_method_invocation_return_value (invocation, NULL); + } + else if (g_strcmp0 (method_name, "RemoveTempFiles") == 0) { + gsd_ldsm_purge_temp_files (now); + g_dbus_method_invocation_return_value (invocation, NULL); + } + g_date_time_unref (now); +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + NULL, /* Get Property */ + NULL, /* Set Property */ +}; + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *res, + GsdHousekeepingManager *manager) +{ + GDBusConnection *connection; + GError *error = NULL; + GDBusInterfaceInfo **infos; + int i; + + connection = g_bus_get_finish (res, &error); + if (connection == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Could not get session bus: %s", error->message); + g_error_free (error); + return; + } + manager->connection = connection; + + infos = manager->introspection_data->interfaces; + for (i = 0; infos[i] != NULL; i++) { + g_dbus_connection_register_object (connection, + GSD_HOUSEKEEPING_DBUS_PATH, + infos[i], + &interface_vtable, + manager, + NULL, + NULL); + } + + manager->name_id = g_bus_own_name_on_connection (connection, + "org.gnome.SettingsDaemon.Housekeeping", + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, + NULL, + NULL, + NULL); +} + +static void +register_manager_dbus (GsdHousekeepingManager *manager) +{ + manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + g_assert (manager->introspection_data != NULL); + manager->bus_cancellable = g_cancellable_new (); + + g_bus_get (G_BUS_TYPE_SESSION, + manager->bus_cancellable, + (GAsyncReadyCallback) on_bus_gotten, + manager); +} + +gboolean +gsd_housekeeping_manager_start (GsdHousekeepingManager *manager, + GError **error) +{ + gchar *dir; + + g_debug ("Starting housekeeping manager"); + gnome_settings_profile_start (NULL); + + /* Create ~/.local/ as early as possible */ + (void) g_mkdir_with_parents(g_get_user_data_dir (), 0700); + + /* Create ~/.local/share/applications/, see + * https://bugzilla.gnome.org/show_bug.cgi?id=703048 */ + dir = g_build_filename (g_get_user_data_dir (), "applications", NULL); + (void) g_mkdir (dir, 0700); + g_free (dir); + + gsd_ldsm_setup (FALSE); + + manager->settings = g_settings_new (THUMB_PREFIX); + g_signal_connect (G_OBJECT (manager->settings), "changed", + G_CALLBACK (settings_changed_callback), manager); + + /* Clean once, a few minutes after start-up */ + do_cleanup_soon (manager); + + /* Clean periodically, on a daily basis. */ + manager->long_term_cb = g_timeout_add_seconds (INTERVAL_ONCE_A_DAY, + (GSourceFunc) do_cleanup, + manager); + g_source_set_name_by_id (manager->long_term_cb, "[gnome-settings-daemon] do_cleanup"); + + gnome_settings_profile_end (NULL); + + return TRUE; +} + +void +gsd_housekeeping_manager_stop (GsdHousekeepingManager *manager) +{ + g_debug ("Stopping housekeeping manager"); + + if (manager->name_id != 0) { + g_bus_unown_name (manager->name_id); + manager->name_id = 0; + } + + g_clear_object (&manager->bus_cancellable); + g_clear_pointer (&manager->introspection_data, g_dbus_node_info_unref); + g_clear_object (&manager->connection); + + if (manager->short_term_cb) { + g_source_remove (manager->short_term_cb); + manager->short_term_cb = 0; + } + + if (manager->long_term_cb) { + g_source_remove (manager->long_term_cb); + manager->long_term_cb = 0; + + /* Do a clean-up on shutdown if and only if the size or age + limits have been set to paranoid levels (zero) */ + if ((g_settings_get_int (manager->settings, THUMB_AGE_KEY) == 0) || + (g_settings_get_int (manager->settings, THUMB_SIZE_KEY) == 0)) { + do_cleanup (manager); + } + + } + + g_clear_object (&manager->settings); + gsd_ldsm_clean (); +} + +static void +gsd_housekeeping_manager_finalize (GObject *object) +{ + gsd_housekeeping_manager_stop (GSD_HOUSEKEEPING_MANAGER (object)); + + G_OBJECT_CLASS (gsd_housekeeping_manager_parent_class)->finalize (object); +} + +static void +gsd_housekeeping_manager_class_init (GsdHousekeepingManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_housekeeping_manager_finalize; + + notify_init ("gnome-settings-daemon"); +} + +static void +gsd_housekeeping_manager_init (GsdHousekeepingManager *manager) +{ +} + +GsdHousekeepingManager * +gsd_housekeeping_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_HOUSEKEEPING_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + + register_manager_dbus (manager_object); + } + + return GSD_HOUSEKEEPING_MANAGER (manager_object); +} diff --git a/plugins/housekeeping/gsd-housekeeping-manager.h b/plugins/housekeeping/gsd-housekeeping-manager.h new file mode 100644 index 0000000..8b5f840 --- /dev/null +++ b/plugins/housekeeping/gsd-housekeeping-manager.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Michael J. Chudobiak <mjc@avtechpulse.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_HOUSEKEEPING_MANAGER_H +#define __GSD_HOUSEKEEPING_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_HOUSEKEEPING_MANAGER (gsd_housekeeping_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdHousekeepingManager, gsd_housekeeping_manager, GSD, HOUSEKEEPING_MANAGER, GObject) + +GsdHousekeepingManager * gsd_housekeeping_manager_new (void); +gboolean gsd_housekeeping_manager_start (GsdHousekeepingManager *manager, + GError **error); +void gsd_housekeeping_manager_stop (GsdHousekeepingManager *manager); + +G_END_DECLS + +#endif /* __GSD_HOUSEKEEPING_MANAGER_H */ diff --git a/plugins/housekeeping/gsd-purge-temp-test.c b/plugins/housekeeping/gsd-purge-temp-test.c new file mode 100644 index 0000000..6ff2d5e --- /dev/null +++ b/plugins/housekeeping/gsd-purge-temp-test.c @@ -0,0 +1,62 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * vim: set et sw=8 ts=8: + * + * Copyright (c) 2008, Novell, Inc. + * + * Authors: Vincent Untz <vuntz@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" +#include <gtk/gtk.h> +#include <libnotify/notify.h> +#include "gsd-disk-space.h" + +int +main (int argc, + char **argv) +{ + GFile *file; + DeleteData *data; + GDateTime *old; + GMainLoop *loop; + g_autofree char *filesystem = NULL; + + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); + + gtk_init (&argc, &argv); + notify_init ("gsd-purge-temp-test"); + loop = g_main_loop_new (NULL, FALSE); + + file = g_file_new_for_path ("/tmp/gsd-purge-temp-test"); + if (!g_file_query_exists (file, NULL)) { + g_warning ("Create /tmp/gsd-purge-temp-test and add some files to it to test deletion by date"); + g_object_unref (file); + return 1; + } + + old = g_date_time_new_now_local (); + filesystem = get_filesystem (file); + data = delete_data_new (file, NULL, old, FALSE, FALSE, 0, filesystem); + delete_recursively_by_age (data); + delete_data_unref (data); + g_object_unref (file); + + g_main_loop_run (loop); + + return 0; +} + diff --git a/plugins/housekeeping/main.c b/plugins/housekeeping/main.c new file mode 100644 index 0000000..89c12f7 --- /dev/null +++ b/plugins/housekeeping/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_housekeeping_manager_new +#define START gsd_housekeeping_manager_start +#define STOP gsd_housekeeping_manager_stop +#define MANAGER GsdHousekeepingManager +#include "gsd-housekeeping-manager.h" + +#include "daemon-skeleton.h" diff --git a/plugins/housekeeping/meson.build b/plugins/housekeeping/meson.build new file mode 100644 index 0000000..a0b4ca5 --- /dev/null +++ b/plugins/housekeeping/meson.build @@ -0,0 +1,41 @@ +common_files = files( + 'gsd-disk-space.c', + 'gsd-disk-space-helper.c' +) + +sources = common_files + files( + 'gsd-housekeeping-manager.c', + 'main.c' +) + +deps = plugins_deps + [ + gio_unix_dep, + gtk_dep, + libnotify_dep +] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) + +programs = [ + 'gsd-disk-space-test', + 'gsd-empty-trash-test', + 'gsd-purge-temp-test' +] + +foreach program: programs + executable( + program, + common_files + [program + '.c'], + include_directories: top_inc, + dependencies: deps + ) +endforeach diff --git a/plugins/keyboard/.indent.pro b/plugins/keyboard/.indent.pro new file mode 100644 index 0000000..bdff074 --- /dev/null +++ b/plugins/keyboard/.indent.pro @@ -0,0 +1,2 @@ +-kr -i8 -pcs -lps -psl + diff --git a/plugins/keyboard/gsd-keyboard-manager.c b/plugins/keyboard/gsd-keyboard-manager.c new file mode 100644 index 0000000..2feb87e --- /dev/null +++ b/plugins/keyboard/gsd-keyboard-manager.c @@ -0,0 +1,718 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright © 2001 Ximian, Inc. + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Written by Sergey V. Oudaltsov <svu@users.sourceforge.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <sys/types.h> +#include <sys/wait.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> + +#include <locale.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <gdk/gdk.h> +#include <gdk/gdkx.h> +#include <gtk/gtk.h> + +#include "gnome-settings-bus.h" +#include "gnome-settings-profile.h" +#include "gsd-keyboard-manager.h" +#include "gsd-input-helper.h" +#include "gsd-enums.h" +#include "gsd-settings-migrate.h" + +#define GSD_KEYBOARD_DIR "org.gnome.settings-daemon.peripherals.keyboard" + +#define KEY_CLICK "click" +#define KEY_CLICK_VOLUME "click-volume" + +#define KEY_BELL_VOLUME "bell-volume" +#define KEY_BELL_PITCH "bell-pitch" +#define KEY_BELL_DURATION "bell-duration" +#define KEY_BELL_MODE "bell-mode" +#define KEY_BELL_CUSTOM_FILE "bell-custom-file" + +#define GNOME_DESKTOP_INTERFACE_DIR "org.gnome.desktop.interface" + +#define KEY_GTK_IM_MODULE "gtk-im-module" +#define GTK_IM_MODULE_SIMPLE "gtk-im-context-simple" +#define GTK_IM_MODULE_IBUS "ibus" + +#define GNOME_DESKTOP_INPUT_SOURCES_DIR "org.gnome.desktop.input-sources" + +#define KEY_INPUT_SOURCES "sources" +#define KEY_KEYBOARD_OPTIONS "xkb-options" + +#define INPUT_SOURCE_TYPE_XKB "xkb" +#define INPUT_SOURCE_TYPE_IBUS "ibus" + +#define DEFAULT_LAYOUT "us" + +#define GNOME_A11Y_APPLICATIONS_INTERFACE_DIR "org.gnome.desktop.a11y.applications" +#define KEY_OSK_ENABLED "screen-keyboard-enabled" + +struct _GsdKeyboardManager +{ + GObject parent; + + guint start_idle_id; + GSettings *settings; + GSettings *input_sources_settings; + GSettings *a11y_settings; + GDBusProxy *localed; + GCancellable *cancellable; + + GdkDeviceManager *device_manager; + guint device_added_id; + guint device_removed_id; +}; + +static void gsd_keyboard_manager_class_init (GsdKeyboardManagerClass *klass); +static void gsd_keyboard_manager_init (GsdKeyboardManager *keyboard_manager); +static void gsd_keyboard_manager_finalize (GObject *object); + +static void update_gtk_im_module (GsdKeyboardManager *manager); + +G_DEFINE_TYPE (GsdKeyboardManager, gsd_keyboard_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +static void +init_builder_with_sources (GVariantBuilder *builder, + GSettings *settings) +{ + const gchar *type; + const gchar *id; + GVariantIter iter; + GVariant *sources; + + sources = g_settings_get_value (settings, KEY_INPUT_SOURCES); + + g_variant_builder_init (builder, G_VARIANT_TYPE ("a(ss)")); + + g_variant_iter_init (&iter, sources); + while (g_variant_iter_next (&iter, "(&s&s)", &type, &id)) + g_variant_builder_add (builder, "(ss)", type, id); + + g_variant_unref (sources); +} + +static gboolean +schema_is_installed (const char *schema) +{ + GSettingsSchemaSource *source = NULL; + gchar **non_relocatable = NULL; + gchar **relocatable = NULL; + gboolean installed = FALSE; + + source = g_settings_schema_source_get_default (); + if (!source) + return FALSE; + + g_settings_schema_source_list_schemas (source, TRUE, &non_relocatable, &relocatable); + + if (g_strv_contains ((const gchar * const *)non_relocatable, schema) || + g_strv_contains ((const gchar * const *)relocatable, schema)) + installed = TRUE; + + g_strfreev (non_relocatable); + g_strfreev (relocatable); + return installed; +} + +static void +apply_bell (GsdKeyboardManager *manager) +{ + GSettings *settings; + XKeyboardControl kbdcontrol; + gboolean click; + int bell_volume; + int bell_pitch; + int bell_duration; + GsdBellMode bell_mode; + int click_volume; + + if (gnome_settings_is_wayland ()) + return; + + g_debug ("Applying the bell settings"); + settings = manager->settings; + click = g_settings_get_boolean (settings, KEY_CLICK); + click_volume = g_settings_get_int (settings, KEY_CLICK_VOLUME); + + bell_pitch = g_settings_get_int (settings, KEY_BELL_PITCH); + bell_duration = g_settings_get_int (settings, KEY_BELL_DURATION); + + bell_mode = g_settings_get_enum (settings, KEY_BELL_MODE); + bell_volume = (bell_mode == GSD_BELL_MODE_ON) ? 50 : 0; + + /* as percentage from 0..100 inclusive */ + if (click_volume < 0) { + click_volume = 0; + } else if (click_volume > 100) { + click_volume = 100; + } + kbdcontrol.key_click_percent = click ? click_volume : 0; + kbdcontrol.bell_percent = bell_volume; + kbdcontrol.bell_pitch = bell_pitch; + kbdcontrol.bell_duration = bell_duration; + + gdk_error_trap_push (); + XChangeKeyboardControl (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), + KBKeyClickPercent | KBBellPercent | KBBellPitch | KBBellDuration, + &kbdcontrol); + + XSync (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), FALSE); + gdk_error_trap_pop_ignored (); +} + +static void +apply_all_settings (GsdKeyboardManager *manager) +{ + apply_bell (manager); +} + +static void +settings_changed (GSettings *settings, + const char *key, + GsdKeyboardManager *manager) +{ + if (g_strcmp0 (key, KEY_CLICK) == 0|| + g_strcmp0 (key, KEY_CLICK_VOLUME) == 0 || + g_strcmp0 (key, KEY_BELL_PITCH) == 0 || + g_strcmp0 (key, KEY_BELL_DURATION) == 0 || + g_strcmp0 (key, KEY_BELL_MODE) == 0) { + g_debug ("Bell setting '%s' changed, applying bell settings", key); + apply_bell (manager); + } else if (g_strcmp0 (key, KEY_BELL_CUSTOM_FILE) == 0){ + g_debug ("Ignoring '%s' setting change", KEY_BELL_CUSTOM_FILE); + } else { + g_warning ("Unhandled settings change, key '%s'", key); + } + +} + +static void +device_added_cb (GdkDeviceManager *device_manager, + GdkDevice *device, + GsdKeyboardManager *manager) +{ + GdkInputSource source; + + source = gdk_device_get_source (device); + if (source == GDK_SOURCE_TOUCHSCREEN) { + update_gtk_im_module (manager); + } +} + +static void +device_removed_cb (GdkDeviceManager *device_manager, + GdkDevice *device, + GsdKeyboardManager *manager) +{ + GdkInputSource source; + + source = gdk_device_get_source (device); + if (source == GDK_SOURCE_TOUCHSCREEN) + update_gtk_im_module (manager); +} + +static void +set_devicepresence_handler (GsdKeyboardManager *manager) +{ + GdkDeviceManager *device_manager; + + if (gnome_settings_is_wayland ()) + return; + + device_manager = gdk_display_get_device_manager (gdk_display_get_default ()); + + manager->device_added_id = g_signal_connect (G_OBJECT (device_manager), "device-added", + G_CALLBACK (device_added_cb), manager); + manager->device_removed_id = g_signal_connect (G_OBJECT (device_manager), "device-removed", + G_CALLBACK (device_removed_cb), manager); + manager->device_manager = device_manager; +} + +static gboolean +need_ibus (GVariant *sources) +{ + GVariantIter iter; + const gchar *type; + + g_variant_iter_init (&iter, sources); + while (g_variant_iter_next (&iter, "(&s&s)", &type, NULL)) + if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS)) + return TRUE; + + return FALSE; +} + +static gboolean +need_osk (GsdKeyboardManager *manager) +{ + gboolean has_touchscreen = FALSE; + GList *devices; + GdkSeat *seat; + + if (g_settings_get_boolean (manager->a11y_settings, + KEY_OSK_ENABLED)) + return TRUE; + + seat = gdk_display_get_default_seat (gdk_display_get_default ()); + devices = gdk_seat_get_slaves (seat, GDK_SEAT_CAPABILITY_TOUCH); + + has_touchscreen = devices != NULL; + + g_list_free (devices); + + return has_touchscreen; +} + +static void +set_gtk_im_module (GsdKeyboardManager *manager, + GSettings *settings, + GVariant *sources) +{ + const gchar *new_module; + gchar *current_module; + + if (need_ibus (sources) || need_osk (manager)) + new_module = GTK_IM_MODULE_IBUS; + else + new_module = GTK_IM_MODULE_SIMPLE; + + current_module = g_settings_get_string (settings, KEY_GTK_IM_MODULE); + if (!g_str_equal (current_module, new_module)) + g_settings_set_string (settings, KEY_GTK_IM_MODULE, new_module); + g_free (current_module); +} + +static void +update_gtk_im_module (GsdKeyboardManager *manager) +{ + GSettings *interface_settings; + GVariant *sources; + + /* Gtk+ uses the IM module advertised in XSETTINGS so, if we + * have IBus input sources, we want it to load that + * module. Otherwise we can use the default "simple" module + * which is builtin gtk+ + */ + interface_settings = g_settings_new (GNOME_DESKTOP_INTERFACE_DIR); + sources = g_settings_get_value (manager->input_sources_settings, + KEY_INPUT_SOURCES); + set_gtk_im_module (manager, interface_settings, sources); + g_object_unref (interface_settings); + g_variant_unref (sources); +} + +static void +get_sources_from_xkb_config (GsdKeyboardManager *manager) +{ + GVariantBuilder builder; + GVariant *v; + gint i, n; + gchar **layouts = NULL; + gchar **variants = NULL; + + v = g_dbus_proxy_get_cached_property (manager->localed, "X11Layout"); + if (v) { + const gchar *s = g_variant_get_string (v, NULL); + if (*s) + layouts = g_strsplit (s, ",", -1); + g_variant_unref (v); + } + + init_builder_with_sources (&builder, manager->input_sources_settings); + + if (!layouts) { + g_variant_builder_add (&builder, "(ss)", INPUT_SOURCE_TYPE_XKB, DEFAULT_LAYOUT); + goto out; + } + + v = g_dbus_proxy_get_cached_property (manager->localed, "X11Variant"); + if (v) { + const gchar *s = g_variant_get_string (v, NULL); + if (*s) + variants = g_strsplit (s, ",", -1); + g_variant_unref (v); + } + + if (variants && variants[0]) + n = MIN (g_strv_length (layouts), g_strv_length (variants)); + else + n = g_strv_length (layouts); + + for (i = 0; i < n && layouts[i][0]; ++i) { + gchar *id; + + if (variants && variants[i] && variants[i][0]) + id = g_strdup_printf ("%s+%s", layouts[i], variants[i]); + else + id = g_strdup (layouts[i]); + + g_variant_builder_add (&builder, "(ss)", INPUT_SOURCE_TYPE_XKB, id); + g_free (id); + } + +out: + g_settings_set_value (manager->input_sources_settings, KEY_INPUT_SOURCES, g_variant_builder_end (&builder)); + + g_strfreev (layouts); + g_strfreev (variants); +} + +static void +get_options_from_xkb_config (GsdKeyboardManager *manager) +{ + GVariant *v; + gchar **options = NULL; + + v = g_dbus_proxy_get_cached_property (manager->localed, "X11Options"); + if (v) { + const gchar *s = g_variant_get_string (v, NULL); + if (*s) + options = g_strsplit (s, ",", -1); + g_variant_unref (v); + } + + if (!options) + return; + + g_settings_set_strv (manager->input_sources_settings, KEY_KEYBOARD_OPTIONS, (const gchar * const*) options); + + g_strfreev (options); +} + +static void +convert_libgnomekbd_options (GSettings *settings) +{ + GPtrArray *opt_array; + GSettings *libgnomekbd_settings; + gchar **options, **o; + + if (!schema_is_installed ("org.gnome.libgnomekbd.keyboard")) + return; + + opt_array = g_ptr_array_new_with_free_func (g_free); + + libgnomekbd_settings = g_settings_new ("org.gnome.libgnomekbd.keyboard"); + options = g_settings_get_strv (libgnomekbd_settings, "options"); + + for (o = options; *o; ++o) { + gchar **strv; + + strv = g_strsplit (*o, "\t", 2); + if (strv[0] && strv[1]) + g_ptr_array_add (opt_array, g_strdup (strv[1])); + g_strfreev (strv); + } + g_ptr_array_add (opt_array, NULL); + + g_settings_set_strv (settings, KEY_KEYBOARD_OPTIONS, (const gchar * const*) opt_array->pdata); + + g_strfreev (options); + g_object_unref (libgnomekbd_settings); + g_ptr_array_free (opt_array, TRUE); +} + +static void +convert_libgnomekbd_layouts (GSettings *settings) +{ + GVariantBuilder builder; + GSettings *libgnomekbd_settings; + gchar **layouts, **l; + + if (!schema_is_installed ("org.gnome.libgnomekbd.keyboard")) + return; + + init_builder_with_sources (&builder, settings); + + libgnomekbd_settings = g_settings_new ("org.gnome.libgnomekbd.keyboard"); + layouts = g_settings_get_strv (libgnomekbd_settings, "layouts"); + + for (l = layouts; *l; ++l) { + gchar *id; + gchar **strv; + + strv = g_strsplit (*l, "\t", 2); + if (strv[0] && !strv[1]) + id = g_strdup (strv[0]); + else if (strv[0] && strv[1]) + id = g_strdup_printf ("%s+%s", strv[0], strv[1]); + else + id = NULL; + + if (id) + g_variant_builder_add (&builder, "(ss)", INPUT_SOURCE_TYPE_XKB, id); + + g_free (id); + g_strfreev (strv); + } + + g_settings_set_value (settings, KEY_INPUT_SOURCES, g_variant_builder_end (&builder)); + + g_strfreev (layouts); + g_object_unref (libgnomekbd_settings); +} + +static void +maybe_convert_old_settings (GSettings *settings) +{ + GVariant *sources; + gchar **options; + gchar *stamp_dir_path = NULL; + gchar *stamp_file_path = NULL; + GError *error = NULL; + + stamp_dir_path = g_build_filename (g_get_user_data_dir (), PACKAGE_NAME, NULL); + if (g_mkdir_with_parents (stamp_dir_path, 0755)) { + g_warning ("Failed to create directory %s: %s", stamp_dir_path, g_strerror (errno)); + goto out; + } + + stamp_file_path = g_build_filename (stamp_dir_path, "input-sources-converted", NULL); + if (g_file_test (stamp_file_path, G_FILE_TEST_EXISTS)) + goto out; + + sources = g_settings_get_value (settings, KEY_INPUT_SOURCES); + if (g_variant_n_children (sources) < 1) { + convert_libgnomekbd_layouts (settings); + } + g_variant_unref (sources); + + options = g_settings_get_strv (settings, KEY_KEYBOARD_OPTIONS); + if (g_strv_length (options) < 1) + convert_libgnomekbd_options (settings); + g_strfreev (options); + + if (!g_file_set_contents (stamp_file_path, "", 0, &error)) { + g_warning ("%s", error->message); + g_error_free (error); + } +out: + g_free (stamp_file_path); + g_free (stamp_dir_path); +} + +static void +maybe_create_initial_settings (GsdKeyboardManager *manager) +{ + GSettings *settings; + GVariant *sources; + gchar **options; + + settings = manager->input_sources_settings; + + if (g_getenv ("RUNNING_UNDER_GDM")) + return; + + maybe_convert_old_settings (settings); + + /* if we still don't have anything do some educated guesses */ + sources = g_settings_get_value (settings, KEY_INPUT_SOURCES); + if (g_variant_n_children (sources) < 1) + get_sources_from_xkb_config (manager); + g_variant_unref (sources); + + options = g_settings_get_strv (settings, KEY_KEYBOARD_OPTIONS); + if (g_strv_length (options) < 1) + get_options_from_xkb_config (manager); + g_strfreev (options); +} + +static void +localed_proxy_ready (GObject *source, + GAsyncResult *res, + gpointer data) +{ + GsdKeyboardManager *manager = data; + GDBusProxy *proxy; + GError *error = NULL; + + proxy = g_dbus_proxy_new_finish (res, &error); + if (!proxy) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_error_free (error); + return; + } + g_warning ("Failed to contact localed: %s", error->message); + g_error_free (error); + } + + manager->localed = proxy; + maybe_create_initial_settings (manager); +} + +static gboolean +start_keyboard_idle_cb (GsdKeyboardManager *manager) +{ + gnome_settings_profile_start (NULL); + + g_debug ("Starting keyboard manager"); + + manager->settings = g_settings_new (GSD_KEYBOARD_DIR); + + set_devicepresence_handler (manager); + + manager->input_sources_settings = g_settings_new (GNOME_DESKTOP_INPUT_SOURCES_DIR); + g_signal_connect_swapped (manager->input_sources_settings, + "changed::" KEY_INPUT_SOURCES, + G_CALLBACK (update_gtk_im_module), manager); + + manager->a11y_settings = g_settings_new (GNOME_A11Y_APPLICATIONS_INTERFACE_DIR); + g_signal_connect_swapped (manager->a11y_settings, + "changed::" KEY_OSK_ENABLED, + G_CALLBACK (update_gtk_im_module), manager); + update_gtk_im_module (manager); + + manager->cancellable = g_cancellable_new (); + + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.locale1", + "/org/freedesktop/locale1", + "org.freedesktop.locale1", + manager->cancellable, + localed_proxy_ready, + manager); + + if (!gnome_settings_is_wayland ()) { + /* apply current settings before we install the callback */ + g_debug ("Started the keyboard plugin, applying all settings"); + apply_all_settings (manager); + + g_signal_connect (G_OBJECT (manager->settings), "changed", + G_CALLBACK (settings_changed), manager); + } + + gnome_settings_profile_end (NULL); + + manager->start_idle_id = 0; + + return FALSE; +} + +gboolean +gsd_keyboard_manager_start (GsdKeyboardManager *manager, + GError **error) +{ + gnome_settings_profile_start (NULL); + + manager->start_idle_id = g_idle_add ((GSourceFunc) start_keyboard_idle_cb, manager); + g_source_set_name_by_id (manager->start_idle_id, "[gnome-settings-daemon] start_keyboard_idle_cb"); + + gnome_settings_profile_end (NULL); + + return TRUE; +} + +void +gsd_keyboard_manager_stop (GsdKeyboardManager *manager) +{ + g_debug ("Stopping keyboard manager"); + + g_cancellable_cancel (manager->cancellable); + g_clear_object (&manager->cancellable); + + g_clear_object (&manager->settings); + g_clear_object (&manager->input_sources_settings); + g_clear_object (&manager->a11y_settings); + g_clear_object (&manager->localed); + + if (manager->device_manager != NULL) { + g_signal_handler_disconnect (manager->device_manager, manager->device_added_id); + g_signal_handler_disconnect (manager->device_manager, manager->device_removed_id); + manager->device_manager = NULL; + } +} + +static void +gsd_keyboard_manager_class_init (GsdKeyboardManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_keyboard_manager_finalize; +} + +static void +gsd_keyboard_manager_init (GsdKeyboardManager *manager) +{ +} + +static void +gsd_keyboard_manager_finalize (GObject *object) +{ + GsdKeyboardManager *keyboard_manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_KEYBOARD_MANAGER (object)); + + keyboard_manager = GSD_KEYBOARD_MANAGER (object); + + g_return_if_fail (keyboard_manager != NULL); + + gsd_keyboard_manager_stop (keyboard_manager); + + if (keyboard_manager->start_idle_id != 0) + g_source_remove (keyboard_manager->start_idle_id); + + G_OBJECT_CLASS (gsd_keyboard_manager_parent_class)->finalize (object); +} + +static void +migrate_keyboard_settings (void) +{ + GsdSettingsMigrateEntry entries[] = { + { "repeat", "repeat", NULL }, + { "repeat-interval", "repeat-interval", NULL }, + { "delay", "delay", NULL }, + { "remember-numlock-state", "remember-numlock-state", NULL }, + }; + + gsd_settings_migrate_check ("org.gnome.settings-daemon.peripherals.keyboard.deprecated", + "/org/gnome/settings-daemon/peripherals/keyboard/", + "org.gnome.desktop.peripherals.keyboard", + "/org/gnome/desktop/peripherals/keyboard/", + entries, G_N_ELEMENTS (entries)); +} + +GsdKeyboardManager * +gsd_keyboard_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + migrate_keyboard_settings (); + manager_object = g_object_new (GSD_TYPE_KEYBOARD_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_KEYBOARD_MANAGER (manager_object); +} diff --git a/plugins/keyboard/gsd-keyboard-manager.h b/plugins/keyboard/gsd-keyboard-manager.h new file mode 100644 index 0000000..e498f31 --- /dev/null +++ b/plugins/keyboard/gsd-keyboard-manager.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_KEYBOARD_MANAGER_H +#define __GSD_KEYBOARD_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_KEYBOARD_MANAGER (gsd_keyboard_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdKeyboardManager, gsd_keyboard_manager, GSD, KEYBOARD_MANAGER, GObject) + +GsdKeyboardManager * gsd_keyboard_manager_new (void); +gboolean gsd_keyboard_manager_start (GsdKeyboardManager *manager, + GError **error); +void gsd_keyboard_manager_stop (GsdKeyboardManager *manager); + +G_END_DECLS + +#endif /* __GSD_KEYBOARD_MANAGER_H */ diff --git a/plugins/keyboard/main.c b/plugins/keyboard/main.c new file mode 100644 index 0000000..48fce06 --- /dev/null +++ b/plugins/keyboard/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_keyboard_manager_new +#define START gsd_keyboard_manager_start +#define STOP gsd_keyboard_manager_stop +#define MANAGER GsdKeyboardManager +#include "gsd-keyboard-manager.h" + +#include "daemon-skeleton-gtk.h" diff --git a/plugins/keyboard/meson.build b/plugins/keyboard/meson.build new file mode 100644 index 0000000..172193f --- /dev/null +++ b/plugins/keyboard/meson.build @@ -0,0 +1,21 @@ +sources = files( + 'gsd-keyboard-manager.c', + 'main.c' +) + +deps = plugins_deps + [ + gtk_dep, + libcommon_dep, + x11_dep +] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, data_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) diff --git a/plugins/media-keys/README.media-keys-API b/plugins/media-keys/README.media-keys-API new file mode 100644 index 0000000..55d1a59 --- /dev/null +++ b/plugins/media-keys/README.media-keys-API @@ -0,0 +1,51 @@ +This is very simple documentation to gnome-settings-daemon's +D-Bus API for media players. + +gnome-settings-daemon will send key press events from multimedia +keys to applications that register their interest in those events. +This allows the play/pause button to control an audio player that's +not focused for example. + +The D-Bus API is described in gsd-media-keys-manager.c (look for +introspection_xml), but a small explanation follows here. + +1. Create yourself a proxy object for the remote interface: +Object path: /org/gnome/SettingsDaemon/MediaKeys +D-Bus name: org.gnome.SettingsDaemon.MediaKeys +Interface name: org.gnome.SettingsDaemon.MediaKeys + +2. Register your application with gnome-settings-daemon +GrabMediaPlayerKeys ("my-application", 0) +with the second argument being the current time (usually 0, +or the time passed to you from an event, such as a mouse click) + +3. Listen to the MediaPlayerKeyPressed() signal + +4. When receiving a MediaPlayerKeyPressed() signal, +check whether the first argument (application) matches +the value you passed to GrabMediaPlayerKeys() and apply the +action depending on the key (2nd argument) + +Possible values of key are: +- Play +- Pause +- Stop +- Previous +- Next +- Rewind +- FastForward +- Repeat +- Shuffle + +5. Every time your application is focused, you should call +GrabMediaPlayerKeys() again, so that gnome-settings-daemon knows +which one was last used. This allows switching between a movie player +and a music player, for example, and have the buttons control the +last used application. + +6. When your application wants to stop using the functionality +it can call ReleaseMediaPlayerKeys(). If your application does +not call ReleaseMediaPlayerKeys() and releases its D-Bus connection +then the application will be automatically removed from the list of +applications held by gnome-settings-daemon. + diff --git a/plugins/media-keys/audio-selection-test.c b/plugins/media-keys/audio-selection-test.c new file mode 100644 index 0000000..d06759f --- /dev/null +++ b/plugins/media-keys/audio-selection-test.c @@ -0,0 +1,263 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2017 Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <gtk/gtk.h> + +#define AUDIO_SELECTION_DBUS_NAME "org.gnome.Shell.AudioDeviceSelection" +#define AUDIO_SELECTION_DBUS_PATH "/org/gnome/Shell/AudioDeviceSelection" +#define AUDIO_SELECTION_DBUS_INTERFACE "org.gnome.Shell.AudioDeviceSelection" + +static guint audio_selection_watch_id; +static guint audio_selection_signal_id; +static GDBusConnection *audio_selection_conn; +static gboolean audio_selection_requested; +static GtkWidget *check_headphones, *check_headset, *check_micro; +static GtkWidget *button, *label; + +/* Copy-paste from gvc-mixer-control.h */ +typedef enum +{ + GVC_HEADSET_PORT_CHOICE_NONE = 0, + GVC_HEADSET_PORT_CHOICE_HEADPHONES = 1 << 0, + GVC_HEADSET_PORT_CHOICE_HEADSET = 1 << 1, + GVC_HEADSET_PORT_CHOICE_MIC = 1 << 2 +} GvcHeadsetPortChoice; + +typedef struct { + GvcHeadsetPortChoice choice; + gchar *name; +} AudioSelectionChoice; + +static AudioSelectionChoice audio_selection_choices[] = { + { GVC_HEADSET_PORT_CHOICE_HEADPHONES, "headphones" }, + { GVC_HEADSET_PORT_CHOICE_HEADSET, "headset" }, + { GVC_HEADSET_PORT_CHOICE_MIC, "microphone" }, +}; + +static void +audio_selection_done (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer data) +{ + const gchar *choice; + + if (!audio_selection_requested) + return; + + choice = NULL; + g_variant_get_child (parameters, 0, "&s", &choice); + if (!choice) + return; + + gtk_label_set_text (GTK_LABEL (label), choice); + + audio_selection_requested = FALSE; +} + +static void +audio_selection_needed (GvcHeadsetPortChoice choices) +{ + gchar *args[G_N_ELEMENTS (audio_selection_choices) + 1]; + guint i, n; + + if (!audio_selection_conn) + return; + + n = 0; + for (i = 0; i < G_N_ELEMENTS (audio_selection_choices); ++i) { + if (choices & audio_selection_choices[i].choice) + args[n++] = audio_selection_choices[i].name; + } + args[n] = NULL; + + audio_selection_requested = TRUE; + g_dbus_connection_call (audio_selection_conn, + AUDIO_SELECTION_DBUS_NAME, + AUDIO_SELECTION_DBUS_PATH, + AUDIO_SELECTION_DBUS_INTERFACE, + "Open", + g_variant_new ("(^as)", args), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL, NULL); +} + +static void +update_ask_button (void) +{ + guint num_buttons = 0; + gboolean active = FALSE; + + /* Need gnome-shell running */ + if (audio_selection_conn == NULL) + goto end; + + /* Need at least 2 choices */ + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (check_headphones))) + num_buttons++; + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (check_headset))) + num_buttons++; + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (check_micro))) + num_buttons++; + + if (num_buttons < 2) + goto end; + + /* And no questions in flight */ + if (audio_selection_requested) + goto end; + + active = TRUE; + +end: + gtk_widget_set_sensitive (GTK_WIDGET (button), active); +} + +static void +audio_selection_appeared (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer data) +{ + audio_selection_conn = connection; + audio_selection_signal_id = + g_dbus_connection_signal_subscribe (connection, + AUDIO_SELECTION_DBUS_NAME, + AUDIO_SELECTION_DBUS_INTERFACE, + "DeviceSelected", + AUDIO_SELECTION_DBUS_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + audio_selection_done, + NULL, + NULL); + update_ask_button (); +} + +static void +audio_selection_vanished (GDBusConnection *connection, + const gchar *name, + gpointer data) +{ + if (audio_selection_signal_id) + g_dbus_connection_signal_unsubscribe (audio_selection_conn, + audio_selection_signal_id); + audio_selection_signal_id = 0; + audio_selection_conn = NULL; + update_ask_button (); +} + +static void +watch_gnome_shell (void) +{ + audio_selection_watch_id = + g_bus_watch_name (G_BUS_TYPE_SESSION, + AUDIO_SELECTION_DBUS_NAME, + G_BUS_NAME_WATCHER_FLAGS_NONE, + audio_selection_appeared, + audio_selection_vanished, + NULL, + NULL); +} + +static void +check_buttons_changed (GtkToggleButton *button, + gpointer user_data) +{ + update_ask_button (); +} + +static void +button_clicked (GtkButton *button, + gpointer user_data) +{ + guint choices = 0; + + gtk_label_set_text (GTK_LABEL (label), ""); + + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (check_headphones))) + choices |= GVC_HEADSET_PORT_CHOICE_HEADPHONES; + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (check_headset))) + choices |= GVC_HEADSET_PORT_CHOICE_HEADSET; + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (check_micro))) + choices |= GVC_HEADSET_PORT_CHOICE_MIC; + + audio_selection_needed (choices); +} + +static void +setup_ui (void) +{ + GtkWidget *window; + GtkWidget *box; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + g_signal_connect (GTK_WINDOW (window), "delete-event", + G_CALLBACK (gtk_main_quit), NULL); + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8); + gtk_container_add (GTK_CONTAINER (window), box); + + check_headphones = gtk_check_button_new_with_label ("Headphones"); + g_signal_connect (check_headphones, "toggled", + G_CALLBACK (check_buttons_changed), NULL); + gtk_container_add (GTK_CONTAINER (box), check_headphones); + + check_headset = gtk_check_button_new_with_label ("Headset"); + g_signal_connect (check_headset, "toggled", + G_CALLBACK (check_buttons_changed), NULL); + gtk_container_add (GTK_CONTAINER (box), check_headset); + + check_micro = gtk_check_button_new_with_label ("Microphone"); + g_signal_connect (check_micro, "toggled", + G_CALLBACK (check_buttons_changed), NULL); + gtk_container_add (GTK_CONTAINER (box), check_micro); + + button = gtk_button_new_with_label ("Ask!"); + g_signal_connect (button, "clicked", + G_CALLBACK (button_clicked), NULL); + gtk_container_add (GTK_CONTAINER (box), button); + gtk_widget_set_sensitive (GTK_WIDGET (button), FALSE); + + label = gtk_label_new (""); + gtk_container_add (GTK_CONTAINER (box), label); + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (check_headphones), TRUE); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (check_headset), TRUE); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (check_micro), TRUE); + + gtk_widget_show_all (window); +} + +int main (int argc, char **argv) +{ + gtk_init (&argc, &argv); + + setup_ui (); + watch_gnome_shell (); + + gtk_main (); + + return 0; +} diff --git a/plugins/media-keys/bus-watch-namespace.c b/plugins/media-keys/bus-watch-namespace.c new file mode 100644 index 0000000..1ffdff4 --- /dev/null +++ b/plugins/media-keys/bus-watch-namespace.c @@ -0,0 +1,347 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Lars Uebernickel <lars.uebernickel@canonical.com> + */ + +#include <gio/gio.h> +#include <string.h> +#include "bus-watch-namespace.h" + +typedef struct +{ + guint id; + gchar *name_space; + GBusNameAppearedCallback appeared_handler; + GBusNameVanishedCallback vanished_handler; + gpointer user_data; + GDestroyNotify user_data_destroy; + + GDBusConnection *connection; + GCancellable *cancellable; + GHashTable *names; + guint subscription_id; +} NamespaceWatcher; + +typedef struct +{ + NamespaceWatcher *watcher; + gchar *name; +} GetNameOwnerData; + +static guint namespace_watcher_next_id; +static GHashTable *namespace_watcher_watchers; + +static void +namespace_watcher_stop (gpointer data) +{ + NamespaceWatcher *watcher = data; + + g_cancellable_cancel (watcher->cancellable); + g_object_unref (watcher->cancellable); + + if (watcher->subscription_id) + g_dbus_connection_signal_unsubscribe (watcher->connection, watcher->subscription_id); + + if (watcher->vanished_handler) + { + GHashTableIter it; + const gchar *name; + + g_hash_table_iter_init (&it, watcher->names); + while (g_hash_table_iter_next (&it, (gpointer *) &name, NULL)) + watcher->vanished_handler (watcher->connection, name, watcher->user_data); + } + + if (watcher->user_data_destroy) + watcher->user_data_destroy (watcher->user_data); + + if (watcher->connection) + { + g_signal_handlers_disconnect_by_func (watcher->connection, namespace_watcher_stop, watcher); + g_object_unref (watcher->connection); + } + + g_hash_table_unref (watcher->names); + + g_hash_table_remove (namespace_watcher_watchers, GUINT_TO_POINTER (watcher->id)); + if (g_hash_table_size (namespace_watcher_watchers) == 0) + g_clear_pointer (&namespace_watcher_watchers, g_hash_table_destroy); + + g_free (watcher); +} + +static void +namespace_watcher_name_appeared (NamespaceWatcher *watcher, + const gchar *name, + const gchar *owner) +{ + /* There's a race between NameOwnerChanged signals arriving and the + * ListNames/GetNameOwner sequence returning, so this function might + * be called more than once for the same name. To ensure that + * appeared_handler is only called once for each name, it is only + * called when inserting the name into watcher->names (each name is + * only inserted once there). + */ + if (g_hash_table_contains (watcher->names, name)) + return; + + g_hash_table_add (watcher->names, g_strdup (name)); + + if (watcher->appeared_handler) + watcher->appeared_handler (watcher->connection, name, owner, watcher->user_data); +} + +static void +namespace_watcher_name_vanished (NamespaceWatcher *watcher, + const gchar *name) +{ + if (g_hash_table_remove (watcher->names, name) && watcher->vanished_handler) + watcher->vanished_handler (watcher->connection, name, watcher->user_data); +} + +static gboolean +dbus_name_has_namespace (const gchar *name, + const gchar *name_space) +{ + gint len_name; + gint len_namespace; + + len_name = strlen (name); + len_namespace = strlen (name_space); + + if (len_name < len_namespace) + return FALSE; + + if (memcmp (name_space, name, len_namespace) != 0) + return FALSE; + + return len_namespace == len_name || name[len_namespace] == '.'; +} + +static void +name_owner_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + NamespaceWatcher *watcher = user_data; + const gchar *name; + const gchar *old_owner; + const gchar *new_owner; + + g_variant_get (parameters, "(&s&s&s)", &name, &old_owner, &new_owner); + + if (old_owner[0] != '\0') + namespace_watcher_name_vanished (watcher, name); + + if (new_owner[0] != '\0') + namespace_watcher_name_appeared (watcher, name, new_owner); +} + +static void +got_name_owner (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GetNameOwnerData *data = user_data; + GError *error = NULL; + GVariant *reply; + const gchar *owner; + + reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + goto out; + } + + if (reply == NULL) + { + if (!g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_NAME_HAS_NO_OWNER)) + g_warning ("bus_watch_namespace: error calling org.freedesktop.DBus.GetNameOwner: %s", error->message); + g_error_free (error); + goto out; + } + + g_variant_get (reply, "(&s)", &owner); + namespace_watcher_name_appeared (data->watcher, data->name, owner); + + g_variant_unref (reply); + +out: + g_free (data->name); + g_slice_free (GetNameOwnerData, data); +} + +static void +names_listed (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + NamespaceWatcher *watcher; + GError *error = NULL; + GVariant *reply; + GVariantIter *iter; + const gchar *name; + + reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + return; + } + + watcher = user_data; + + if (reply == NULL) + { + g_warning ("bus_watch_namespace: error calling org.freedesktop.DBus.ListNames: %s", error->message); + g_error_free (error); + return; + } + + g_variant_get (reply, "(as)", &iter); + while (g_variant_iter_next (iter, "&s", &name)) + { + if (dbus_name_has_namespace (name, watcher->name_space)) + { + GetNameOwnerData *data = g_slice_new (GetNameOwnerData); + data->watcher = watcher; + data->name = g_strdup (name); + g_dbus_connection_call (watcher->connection, "org.freedesktop.DBus", "/", + "org.freedesktop.DBus", "GetNameOwner", + g_variant_new ("(s)", name), G_VARIANT_TYPE ("(s)"), + G_DBUS_CALL_FLAGS_NONE, -1, watcher->cancellable, + got_name_owner, data); + } + } + + g_variant_iter_free (iter); + g_variant_unref (reply); +} + +static void +connection_closed (GDBusConnection *connection, + gboolean remote_peer_vanished, + GError *error, + gpointer user_data) +{ + NamespaceWatcher *watcher = user_data; + + namespace_watcher_stop (watcher); +} + +static void +got_bus (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GDBusConnection *connection; + NamespaceWatcher *watcher; + GError *error = NULL; + + connection = g_bus_get_finish (result, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + return; + } + + watcher = user_data; + + if (connection == NULL) + { + namespace_watcher_stop (watcher); + return; + } + + watcher->connection = connection; + g_signal_connect (watcher->connection, "closed", G_CALLBACK (connection_closed), watcher); + + watcher->subscription_id = + g_dbus_connection_signal_subscribe (watcher->connection, "org.freedesktop.DBus", + "org.freedesktop.DBus", "NameOwnerChanged", "/org/freedesktop/DBus", + watcher->name_space, G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE, + name_owner_changed, watcher, NULL); + + g_dbus_connection_call (watcher->connection, "org.freedesktop.DBus", "/", + "org.freedesktop.DBus", "ListNames", NULL, G_VARIANT_TYPE ("(as)"), + G_DBUS_CALL_FLAGS_NONE, -1, watcher->cancellable, + names_listed, watcher); +} + +guint +bus_watch_namespace (GBusType bus_type, + const gchar *name_space, + GBusNameAppearedCallback appeared_handler, + GBusNameVanishedCallback vanished_handler, + gpointer user_data, + GDestroyNotify user_data_destroy) +{ + NamespaceWatcher *watcher; + + /* same rules for interfaces and well-known names */ + g_return_val_if_fail (name_space != NULL && g_dbus_is_interface_name (name_space), 0); + g_return_val_if_fail (appeared_handler || vanished_handler, 0); + + watcher = g_new0 (NamespaceWatcher, 1); + watcher->id = namespace_watcher_next_id++; + watcher->name_space = g_strdup (name_space); + watcher->appeared_handler = appeared_handler; + watcher->vanished_handler = vanished_handler; + watcher->user_data = user_data; + watcher->user_data_destroy = user_data_destroy; + watcher->cancellable = g_cancellable_new ();; + watcher->names = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + if (namespace_watcher_watchers == NULL) + namespace_watcher_watchers = g_hash_table_new (g_direct_hash, g_direct_equal); + g_hash_table_insert (namespace_watcher_watchers, GUINT_TO_POINTER (watcher->id), watcher); + + g_bus_get (bus_type, watcher->cancellable, got_bus, watcher); + + return watcher->id; +} + +void +bus_unwatch_namespace (guint id) +{ + /* namespace_watcher_stop() might have already removed the watcher + * with @id in the case of a connection error. Thus, this function + * doesn't warn when @id is absent from the hash table. + */ + + if (namespace_watcher_watchers) + { + NamespaceWatcher *watcher; + + watcher = g_hash_table_lookup (namespace_watcher_watchers, GUINT_TO_POINTER (id)); + if (watcher) + { + /* make sure vanished() is not called as a result of this function */ + g_hash_table_remove_all (watcher->names); + + namespace_watcher_stop (watcher); + } + } +} diff --git a/plugins/media-keys/bus-watch-namespace.h b/plugins/media-keys/bus-watch-namespace.h new file mode 100644 index 0000000..215f6be --- /dev/null +++ b/plugins/media-keys/bus-watch-namespace.h @@ -0,0 +1,34 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Lars Uebernickel <lars.uebernickel@canonical.com> + */ + +#ifndef __BUS_WATCH_NAMESPACE_H__ +#define __BUS_WATCH_NAMESPACE_H__ + +#include <gio/gio.h> + +guint bus_watch_namespace (GBusType bus_type, + const gchar *name_space, + GBusNameAppearedCallback appeared_handler, + GBusNameVanishedCallback vanished_handler, + gpointer user_data, + GDestroyNotify user_data_destroy); + +void bus_unwatch_namespace (guint id); + +#endif diff --git a/plugins/media-keys/gsd-marshal.list b/plugins/media-keys/gsd-marshal.list new file mode 100644 index 0000000..72f9937 --- /dev/null +++ b/plugins/media-keys/gsd-marshal.list @@ -0,0 +1 @@ +VOID:STRING,STRING diff --git a/plugins/media-keys/gsd-media-keys-manager.c b/plugins/media-keys/gsd-media-keys-manager.c new file mode 100644 index 0000000..42fa14d --- /dev/null +++ b/plugins/media-keys/gsd-media-keys-manager.c @@ -0,0 +1,3920 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2001-2003 Bastien Nocera <hadess@hadess.net> + * Copyright (C) 2006-2007 William Jon McCann <mccann@jhu.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <sys/types.h> +#include <sys/wait.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <math.h> + +#include <locale.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <gio/gio.h> +#include <gdk/gdk.h> +#include <gtk/gtk.h> +#include <gio/gdesktopappinfo.h> +#include <gio/gunixfdlist.h> + +#include <libupower-glib/upower.h> +#include <gdesktop-enums.h> +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include <libgnome-desktop/gnome-systemd.h> + +#if HAVE_GUDEV +#include <gudev/gudev.h> +#endif + +#include "gsd-settings-migrate.h" + +#include "mpris-controller.h" +#include "gnome-settings-bus.h" +#include "gnome-settings-profile.h" +#include "gsd-marshal.h" +#include "gsd-media-keys-manager.h" + +#include "shortcuts-list.h" +#include "shell-key-grabber.h" +#include "gsd-screenshot-utils.h" +#include "gsd-input-helper.h" +#include "gsd-enums.h" +#include "gsd-shell-helper.h" + +#include <canberra.h> +#include <pulse/pulseaudio.h> +#include "gvc-mixer-control.h" +#include "gvc-mixer-sink.h" + +#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon" +#define GSD_DBUS_NAME "org.gnome.SettingsDaemon" +#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon" + +#define GSD_MEDIA_KEYS_DBUS_PATH GSD_DBUS_PATH "/MediaKeys" +#define GSD_MEDIA_KEYS_DBUS_NAME GSD_DBUS_NAME ".MediaKeys" + +#define GNOME_KEYRING_DBUS_NAME "org.gnome.keyring" +#define GNOME_KEYRING_DBUS_PATH "/org/gnome/keyring/daemon" +#define GNOME_KEYRING_DBUS_INTERFACE "org.gnome.keyring.Daemon" + +#define SHELL_DBUS_NAME "org.gnome.Shell" +#define SHELL_DBUS_PATH "/org/gnome/Shell" + +#define CUSTOM_BINDING_SCHEMA SETTINGS_BINDING_DIR ".custom-keybinding" + +#define SETTINGS_SOUND_DIR "org.gnome.desktop.sound" +#define ALLOW_VOLUME_ABOVE_100_PERCENT_KEY "allow-volume-above-100-percent" + +#define SHELL_GRABBER_CALL_TIMEOUT G_MAXINT +#define SHELL_GRABBER_RETRY_INTERVAL_MS 1000 + +/* How long to suppress power-button presses after resume, + * 3 seconds is the minimum necessary to make resume reliable */ +#define GSD_REENABLE_POWER_BUTTON_DELAY 3000 /* ms */ + +static const gchar introspection_xml[] = +"<node name='/org/gnome/SettingsDaemon/MediaKeys'>" +" <interface name='org.gnome.SettingsDaemon.MediaKeys'>" +" <annotation name='org.freedesktop.DBus.GLib.CSymbol' value='gsd_media_keys_manager'/>" +" <method name='GrabMediaPlayerKeys'>" +" <arg name='application' direction='in' type='s'/>" +" <arg name='time' direction='in' type='u'/>" +" </method>" +" <method name='ReleaseMediaPlayerKeys'>" +" <arg name='application' direction='in' type='s'/>" +" </method>" +" <signal name='MediaPlayerKeyPressed'>" +" <arg name='application' type='s'/>" +" <arg name='key' type='s'/>" +" </signal>" +" </interface>" +"</node>"; + +#define SETTINGS_INTERFACE_DIR "org.gnome.desktop.interface" +#define SETTINGS_POWER_DIR "org.gnome.settings-daemon.plugins.power" +#define SETTINGS_XSETTINGS_DIR "org.gnome.settings-daemon.plugins.xsettings" +#define SETTINGS_TOUCHPAD_DIR "org.gnome.desktop.peripherals.touchpad" +#define TOUCHPAD_ENABLED_KEY "send-events" +#define HIGH_CONTRAST "HighContrast" + +#define VOLUME_STEP "volume-step" +#define VOLUME_STEP_PRECISE 2 +#define MAX_VOLUME 65536.0 + +#define SYSTEMD_DBUS_NAME "org.freedesktop.login1" +#define SYSTEMD_DBUS_PATH "/org/freedesktop/login1" +#define SYSTEMD_DBUS_INTERFACE "org.freedesktop.login1.Manager" + +#define AUDIO_SELECTION_DBUS_NAME "org.gnome.Shell.AudioDeviceSelection" +#define AUDIO_SELECTION_DBUS_PATH "/org/gnome/Shell/AudioDeviceSelection" +#define AUDIO_SELECTION_DBUS_INTERFACE "org.gnome.Shell.AudioDeviceSelection" + +#define GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE(o) (gsd_media_keys_manager_get_instance_private (o)) + +typedef struct { + char *application; + char *dbus_name; + guint32 time; + guint watch_id; +} MediaPlayer; + +typedef struct { + gint ref_count; + + MediaKeyType key_type; + ShellActionMode modes; + MetaKeyBindingFlags grab_flags; + const char *settings_key; + gboolean static_setting; + char *custom_path; + char *custom_command; + GArray *accel_ids; +} MediaKey; + +typedef struct { + GsdMediaKeysManager *manager; + GPtrArray *keys; + + /* NOTE: This is to implement a custom cancellation handling where + * we immediately emit an ungrab call if grabbing was cancelled. + */ + gboolean cancelled; +} GrabUngrabData; + +typedef struct +{ + /* Volume bits */ + GvcMixerControl *volume; + GvcMixerStream *sink; + GvcMixerStream *source; + ca_context *ca; + GSettings *sound_settings; + pa_volume_t max_volume; + GtkSettings *gtksettings; +#if HAVE_GUDEV + GHashTable *streams; /* key = X device ID, value = stream id */ + GUdevClient *udev_client; +#endif /* HAVE_GUDEV */ + guint audio_selection_watch_id; + guint audio_selection_signal_id; + GDBusConnection *audio_selection_conn; + gboolean audio_selection_requested; + guint audio_selection_device_id; + + GSettings *settings; + GHashTable *custom_settings; + + GPtrArray *keys; + + /* HighContrast theme settings */ + GSettings *interface_settings; + char *icon_theme; + char *gtk_theme; + + /* Power stuff */ + GSettings *power_settings; + GDBusProxy *power_proxy; + GDBusProxy *power_screen_proxy; + GDBusProxy *power_keyboard_proxy; + UpDevice *composite_device; + char *chassis_type; + gboolean power_button_disabled; + guint reenable_power_button_timer_id; + + /* Shell stuff */ + GsdShell *shell_proxy; + ShellKeyGrabber *key_grabber; + GCancellable *grab_cancellable; + GHashTable *keys_to_sync; + guint keys_sync_source_id; + GrabUngrabData *keys_sync_data; + + /* ScreenSaver stuff */ + GsdScreenSaver *screen_saver_proxy; + + /* Screencast stuff */ + GDBusProxy *screencast_proxy; + guint screencast_timeout_id; + gboolean screencast_recording; + GCancellable *screencast_cancellable; + + /* Rotation */ + guint iio_sensor_watch_id; + gboolean has_accel; + GDBusProxy *iio_sensor_proxy; + + /* RFKill stuff */ + guint rfkill_watch_id; + GDBusProxy *rfkill_proxy; + GCancellable *rfkill_cancellable; + + /* systemd stuff */ + GDBusProxy *logind_proxy; + gint inhibit_keys_fd; + gint inhibit_suspend_fd; + gboolean inhibit_suspend_taken; + + GList *media_players; + + GDBusNodeInfo *introspection_data; + GDBusConnection *connection; + GCancellable *bus_cancellable; + + guint start_idle_id; + + /* Multimedia keys */ + guint mmkeys_name_id; + MprisController *mpris_controller; +} GsdMediaKeysManagerPrivate; + +static void gsd_media_keys_manager_class_init (GsdMediaKeysManagerClass *klass); +static void gsd_media_keys_manager_init (GsdMediaKeysManager *media_keys_manager); +static void gsd_media_keys_manager_finalize (GObject *object); +static void register_manager (GsdMediaKeysManager *manager); +static void custom_binding_changed (GSettings *settings, + const char *settings_key, + GsdMediaKeysManager *manager); +static void keys_sync_queue (GsdMediaKeysManager *manager, + gboolean immediate, + gboolean retry); +static void keys_sync_continue (GsdMediaKeysManager *manager); + + +G_DEFINE_TYPE_WITH_PRIVATE (GsdMediaKeysManager, gsd_media_keys_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +static void +media_key_unref (MediaKey *key) +{ + if (key == NULL) + return; + if (!g_atomic_int_dec_and_test (&key->ref_count)) + return; + g_clear_pointer (&key->accel_ids, g_array_unref); + g_free (key->custom_path); + g_free (key->custom_command); + g_free (key); +} + +static MediaKey * +media_key_ref (MediaKey *key) +{ + g_atomic_int_inc (&key->ref_count); + return key; +} + +static MediaKey * +media_key_new (void) +{ + MediaKey *key = g_new0 (MediaKey, 1); + + key->accel_ids = g_array_new (FALSE, TRUE, sizeof(guint)); + + return media_key_ref (key); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MediaKey, media_key_unref) + +static void +grab_ungrab_data_free (GrabUngrabData *data) +{ + /* NOTE: The manager pointer is not owned and is invalid if the + * operation was cancelled. + */ + + if (!data->cancelled) { + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (data->manager); + + if (priv->keys_sync_data == data) + priv->keys_sync_data = NULL; + } + + data->manager = NULL; + g_clear_pointer (&data->keys, g_ptr_array_unref); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GrabUngrabData, grab_ungrab_data_free) + +static void +set_launch_context_env (GsdMediaKeysManager *manager, + GAppLaunchContext *launch_context) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GError *error = NULL; + GVariant *variant, *item; + GVariantIter *iter; + + variant = g_dbus_connection_call_sync (priv->connection, + GNOME_KEYRING_DBUS_NAME, + GNOME_KEYRING_DBUS_PATH, + GNOME_KEYRING_DBUS_INTERFACE, + "GetEnvironment", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &error); + if (variant == NULL) { + g_warning ("Failed to call GetEnvironment on keyring daemon: %s", error->message); + g_error_free (error); + return; + } + + g_variant_get (variant, "(a{ss})", &iter); + + while ((item = g_variant_iter_next_value (iter))) { + char *key; + char *value; + + g_variant_get (item, + "{ss}", + &key, + &value); + + g_app_launch_context_setenv (launch_context, key, value); + + g_variant_unref (item); + g_free (key); + g_free (value); + } + + g_variant_iter_free (iter); + g_variant_unref (variant); +} + +static char * +get_key_string (MediaKey *key) +{ + if (key->settings_key != NULL) + return g_strdup_printf ("settings:%s", key->settings_key); + else if (key->custom_path != NULL) + return g_strdup_printf ("custom:%s", key->custom_path); + else + g_assert_not_reached (); +} + +static GStrv +get_bindings (GsdMediaKeysManager *manager, + MediaKey *key) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GPtrArray *array; + gchar *binding; + + if (key->settings_key != NULL) { + g_autofree gchar *static_settings_key = NULL; + g_autofree GStrv keys = NULL; + g_autofree GStrv static_keys = NULL; + gchar **item; + + if (!key->static_setting) + return g_settings_get_strv (priv->settings, key->settings_key); + + static_settings_key = g_strconcat (key->settings_key, "-static", NULL); + keys = g_settings_get_strv (priv->settings, key->settings_key); + static_keys = g_settings_get_strv (priv->settings, static_settings_key); + + array = g_ptr_array_new (); + /* Steals all strings from the settings */ + for (item = keys; *item; item++) + g_ptr_array_add (array, *item); + for (item = static_keys; *item; item++) + g_ptr_array_add (array, *item); + g_ptr_array_add (array, NULL); + + return (GStrv) g_ptr_array_free (array, FALSE); + } + + else if (key->custom_path != NULL) { + GSettings *settings; + + settings = g_hash_table_lookup (priv->custom_settings, + key->custom_path); + binding = g_settings_get_string (settings, "binding"); + } else + g_assert_not_reached (); + + array = g_ptr_array_new (); + g_ptr_array_add (array, binding); + g_ptr_array_add (array, NULL); + + return (GStrv) g_ptr_array_free (array, FALSE); +} + +static void +show_osd_with_max_level (GsdMediaKeysManager *manager, + const char *icon, + const char *label, + double level, + double max_level, + const gchar *connector) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->shell_proxy == NULL) + return; + + shell_show_osd_with_max_level (priv->shell_proxy, + icon, label, level, max_level, connector); +} + +static void +show_osd (GsdMediaKeysManager *manager, + const char *icon, + const char *label, + double level, + const char *connector) +{ + show_osd_with_max_level(manager, + icon, label, level, -1, connector); +} + +static const char * +get_icon_name_for_volume (gboolean is_mic, + gboolean muted, + double volume) +{ + static const char *icon_names[] = { + "audio-volume-muted-symbolic", + "audio-volume-low-symbolic", + "audio-volume-medium-symbolic", + "audio-volume-high-symbolic", + "audio-volume-overamplified-symbolic", + NULL + }; + static const char *mic_icon_names[] = { + "microphone-sensitivity-muted-symbolic", + "microphone-sensitivity-low-symbolic", + "microphone-sensitivity-medium-symbolic", + "microphone-sensitivity-high-symbolic", + NULL + }; + int n; + + if (muted) { + n = 0; + } else { + /* select image */ + n = ceill (3.0 * volume); + if (n < 1) + n = 1; + /* output volume above 100% */ + else if (n > 3 && !is_mic) + n = 4; + else if (n > 3) + n = 3; + } + + if (is_mic) + return mic_icon_names[n]; + else + return icon_names[n]; +} + +static void +ungrab_accelerators_complete (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GrabUngrabData) data = user_data; + gboolean success = FALSE; + g_autoptr(GError) error = NULL; + gint i; + + g_debug ("Ungrab call completed!"); + + if (!shell_key_grabber_call_ungrab_accelerators_finish (SHELL_KEY_GRABBER (object), + &success, result, &error)) { + g_warning ("Failed to ungrab accelerators: %s", error->message); + + if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) { + keys_sync_queue (data->manager, FALSE, TRUE); + return; + } + + /* We are screwed at this point; we'll still keep going assuming that we don't + * have the bindings registered anymore. + * The only alternative would be to die and force cleanup of all registered + * grabs that way. + */ + } else if (!success) { + g_warning ("Failed to ungrab some accelerators, they were probably not registered!"); + } + + /* Clear the accelerator IDs. */ + for (i = 0; i < data->keys->len; i++) { + MediaKey *key; + + key = g_ptr_array_index (data->keys, i); + + /* Always clear, as it would just fail again the next time. */ + g_array_set_size (key->accel_ids, 0); + } + + /* Nothing left to do if the operation was cancelled */ + if (data->cancelled) + return; + + keys_sync_continue (data->manager); +} + +static void +grab_accelerators_complete (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GrabUngrabData) data = user_data; + g_autoptr(GVariant) actions = NULL; + g_autoptr(GError) error = NULL; + gint i; + + g_debug ("Grab call completed!"); + + if (!shell_key_grabber_call_grab_accelerators_finish (SHELL_KEY_GRABBER (object), + &actions, result, &error)) { + g_warning ("Failed to grab accelerators: %s", error->message); + + if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) { + keys_sync_queue (data->manager, FALSE, TRUE); + return; + } + + /* We are screwed at this point as we can't grab the keys. Most likely + * this means we are not running on GNOME, or ran into some other weird + * error. + * Either way, finish the operation as there is no way we can recover + * from this. + */ + keys_sync_continue (data->manager); + return; + } + + /* Do an immediate ungrab if the operation was cancelled. + * This may happen on daemon shutdown for example. */ + if (data->cancelled) { + g_debug ("Doing an immediate ungrab on the grabbed accelerators!"); + + shell_key_grabber_call_ungrab_accelerators (SHELL_KEY_GRABBER (object), + actions, + NULL, + ungrab_accelerators_complete, + g_steal_pointer (&data)); + + return; + } + + /* We need to stow away the accel_ids that have been registered successfully. */ + for (i = 0; i < data->keys->len; i++) { + MediaKey *key; + + key = g_ptr_array_index (data->keys, i); + g_assert (key->accel_ids->len == 0); + } + for (i = 0; i < data->keys->len; i++) { + MediaKey *key; + guint accel_id; + + key = g_ptr_array_index (data->keys, i); + + g_variant_get_child (actions, i, "u", &accel_id); + if (accel_id == 0) { + g_autofree gchar *tmp = NULL; + tmp = get_key_string (key); + g_warning ("Failed to grab accelerator for keybinding %s", tmp); + } else { + g_array_append_val (key->accel_ids, accel_id); + } + } + + keys_sync_continue (data->manager); +} + +static void +keys_sync_continue (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + g_auto(GVariantBuilder) ungrab_builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("au")); + g_auto(GVariantBuilder) grab_builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("a(suu)")); + g_autoptr(GPtrArray) keys_being_ungrabbed = NULL; + g_autoptr(GPtrArray) keys_being_grabbed = NULL; + g_autoptr(GrabUngrabData) data = NULL; + GHashTableIter iter; + MediaKey *key; + gboolean need_ungrab = FALSE; + + /* Syncing keys is a two step process in principle, i.e. we first ungrab all keys + * and then grab the new ones. + * To make this work, this function will be called multiple times and it will + * either emit an ungrab or grab call or do nothing when done. + */ + + /* If the keys_to_sync hash table is empty at this point, then we are done. + * priv->keys_sync_data will be cleared automatically when it is unref'ed. + */ + if (g_hash_table_size (priv->keys_to_sync) == 0) + return; + + keys_being_ungrabbed = g_ptr_array_new_with_free_func ((GDestroyNotify) media_key_unref); + keys_being_grabbed = g_ptr_array_new_with_free_func ((GDestroyNotify) media_key_unref); + + g_hash_table_iter_init (&iter, priv->keys_to_sync); + while (g_hash_table_iter_next (&iter, (gpointer*) &key, NULL)) { + g_auto(GStrv) bindings = NULL; + gchar **pos = NULL; + gint i; + + for (i = 0; i < key->accel_ids->len; i++) { + g_variant_builder_add (&ungrab_builder, "u", g_array_index (key->accel_ids, guint, i)); + g_ptr_array_add (keys_being_ungrabbed, media_key_ref (key)); + + need_ungrab = TRUE; + } + + /* Keys that are synced but aren't in the internal list are being removed. */ + if (!g_ptr_array_find (priv->keys, key, NULL)) + continue; + + bindings = get_bindings (manager, key); + pos = bindings; + while (*pos) { + /* Do not try to register empty keybindings. */ + if (strlen (*pos) > 0) { + g_variant_builder_add (&grab_builder, "(suu)", *pos, key->modes, key->grab_flags); + g_ptr_array_add (keys_being_grabbed, media_key_ref (key)); + } + pos++; + } + } + + data = g_new0 (GrabUngrabData, 1); + data->manager = manager; + + /* These calls intentionally do not get a cancellable. See comment in + * GrabUngrabData. + */ + priv->keys_sync_data = data; + + if (need_ungrab) { + data->keys = g_steal_pointer (&keys_being_ungrabbed); + + shell_key_grabber_call_ungrab_accelerators (priv->key_grabber, + g_variant_builder_end (&ungrab_builder), + NULL, + ungrab_accelerators_complete, + g_steal_pointer (&data)); + } else { + data->keys = g_steal_pointer (&keys_being_grabbed); + + g_hash_table_remove_all (priv->keys_to_sync); + + shell_key_grabber_call_grab_accelerators (priv->key_grabber, + g_variant_builder_end (&grab_builder), + NULL, + grab_accelerators_complete, + g_steal_pointer (&data)); + } +} + +static gboolean +keys_sync_start (gpointer user_data) +{ + GsdMediaKeysManager *manager = GSD_MEDIA_KEYS_MANAGER (user_data); + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + priv->keys_sync_source_id = 0; + g_assert (priv->keys_sync_data == NULL); + keys_sync_continue (manager); + + return G_SOURCE_REMOVE; +} + +void +keys_sync_queue (GsdMediaKeysManager *manager, gboolean immediate, gboolean retry) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + guint i; + + if (priv->keys_sync_source_id) + g_source_remove (priv->keys_sync_source_id); + + if (retry) { + /* Abort the currently running operation, and don't retry + * immediately to avoid race condition if an operation was + * already active. */ + if (priv->keys_sync_data) { + priv->keys_sync_data->cancelled = TRUE; + priv->keys_sync_data = NULL; + + immediate = FALSE; + } + + /* Mark all existing keys for sync. */ + for (i = 0; i < priv->keys->len; i++) { + MediaKey *key = g_ptr_array_index (priv->keys, i); + g_hash_table_add (priv->keys_to_sync, media_key_ref (key)); + } + } else if (priv->keys_sync_data) { + /* We are already actively syncing, no need to do anything. */ + return; + } + + priv->keys_sync_source_id = + g_timeout_add (immediate ? 0 : (retry ? SHELL_GRABBER_RETRY_INTERVAL_MS : 50), + keys_sync_start, + manager); +} + +static void +gsettings_changed_cb (GSettings *settings, + const gchar *settings_key, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + int i; + + /* Give up if we don't have proxy to the shell */ + if (!priv->key_grabber) + return; + + /* handled in gsettings_custom_changed_cb() */ + if (g_str_equal (settings_key, "custom-keybindings")) + return; + + /* not needed here */ + if (g_str_equal (settings_key, "max-screencast-length")) + return; + + /* Find the key that was modified */ + if (priv->keys == NULL) + return; + + for (i = 0; i < priv->keys->len; i++) { + MediaKey *key; + + key = g_ptr_array_index (priv->keys, i); + + /* Skip over hard-coded and GConf keys */ + if (key->settings_key == NULL) + continue; + if (strcmp (settings_key, key->settings_key) == 0) { + g_hash_table_add (priv->keys_to_sync, media_key_ref (key)); + keys_sync_queue (manager, FALSE, FALSE); + break; + } + } +} + +static MediaKey * +media_key_new_for_path (GsdMediaKeysManager *manager, + char *path) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GSettings *settings; + char *command, *binding; + MediaKey *key; + + g_debug ("media_key_new_for_path: %s", path); + + settings = g_hash_table_lookup (priv->custom_settings, path); + if (settings == NULL) { + settings = g_settings_new_with_path (CUSTOM_BINDING_SCHEMA, path); + + g_signal_connect (settings, "changed", + G_CALLBACK (custom_binding_changed), manager); + g_hash_table_insert (priv->custom_settings, + g_strdup (path), settings); + } + + command = g_settings_get_string (settings, "command"); + binding = g_settings_get_string (settings, "binding"); + + if (*command == '\0' && *binding == '\0') { + g_debug ("Key binding (%s) is incomplete", path); + g_free (command); + g_free (binding); + return NULL; + } + g_free (binding); + + key = media_key_new (); + key->key_type = CUSTOM_KEY; + key->modes = GSD_ACTION_MODE_LAUNCHER; + key->custom_path = g_strdup (path); + key->custom_command = command; + key->grab_flags = META_KEY_BINDING_NONE; + + return key; +} + +static void +update_custom_binding (GsdMediaKeysManager *manager, + char *path) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + MediaKey *key; + int i; + + /* Remove the existing key */ + for (i = 0; i < priv->keys->len; i++) { + key = g_ptr_array_index (priv->keys, i); + + if (key->custom_path == NULL) + continue; + if (strcmp (key->custom_path, path) == 0) { + g_debug ("Removing custom key binding %s", path); + g_hash_table_add (priv->keys_to_sync, media_key_ref (key)); + g_ptr_array_remove_index_fast (priv->keys, i); + break; + } + } + + /* And create a new one! */ + key = media_key_new_for_path (manager, path); + if (key) { + g_debug ("Adding new custom key binding %s", path); + g_ptr_array_add (priv->keys, key); + + g_hash_table_add (priv->keys_to_sync, media_key_ref (key)); + } + + keys_sync_queue (manager, FALSE, FALSE); +} + +static void +update_custom_binding_command (GsdMediaKeysManager *manager, + GSettings *settings, + char *path) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + MediaKey *key; + int i; + + for (i = 0; i < priv->keys->len; i++) { + key = g_ptr_array_index (priv->keys, i); + + if (key->custom_path == NULL) + continue; + if (strcmp (key->custom_path, path) == 0) { + g_free (key->custom_command); + key->custom_command = g_settings_get_string (settings, "command"); + break; + } + } +} + +static void +custom_binding_changed (GSettings *settings, + const char *settings_key, + GsdMediaKeysManager *manager) +{ + char *path; + + g_object_get (settings, "path", &path, NULL); + + if (strcmp (settings_key, "binding") == 0) + update_custom_binding (manager, path); + else if (strcmp (settings_key, "command") == 0) + update_custom_binding_command (manager, settings, path); + + g_free (path); +} + +static void +gsettings_custom_changed_cb (GSettings *settings, + const char *settings_key, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + char **bindings; + int i, j, n_bindings; + + bindings = g_settings_get_strv (settings, settings_key); + n_bindings = g_strv_length (bindings); + + /* Handle additions */ + for (i = 0; i < n_bindings; i++) { + if (g_hash_table_lookup (priv->custom_settings, + bindings[i])) + continue; + update_custom_binding (manager, bindings[i]); + } + + /* Handle removals */ + for (i = 0; i < priv->keys->len; i++) { + gboolean found = FALSE; + MediaKey *key = g_ptr_array_index (priv->keys, i); + if (key->key_type != CUSTOM_KEY) + continue; + + for (j = 0; j < n_bindings && !found; j++) + found = strcmp (bindings[j], key->custom_path) == 0; + + if (found) + continue; + + g_hash_table_add (priv->keys_to_sync, media_key_ref (key)); + g_hash_table_remove (priv->custom_settings, + key->custom_path); + g_ptr_array_remove_index_fast (priv->keys, i); + --i; /* make up for the removed key */ + } + keys_sync_queue (manager, FALSE, FALSE); + g_strfreev (bindings); +} + +static void +add_key (GsdMediaKeysManager *manager, guint i) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + MediaKey *key; + + key = media_key_new (); + key->key_type = media_keys[i].key_type; + key->settings_key = media_keys[i].settings_key; + key->static_setting = media_keys[i].static_setting; + key->modes = media_keys[i].modes; + key->grab_flags = media_keys[i].grab_flags; + + g_ptr_array_add (priv->keys, key); +} + +static void +init_kbd (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + char **custom_paths; + int i; + + gnome_settings_profile_start (NULL); + + for (i = 0; i < G_N_ELEMENTS (media_keys); i++) + add_key (manager, i); + + /* Custom shortcuts */ + custom_paths = g_settings_get_strv (priv->settings, + "custom-keybindings"); + + for (i = 0; i < g_strv_length (custom_paths); i++) { + MediaKey *key; + + g_debug ("Setting up custom keybinding %s", custom_paths[i]); + + key = media_key_new_for_path (manager, custom_paths[i]); + if (!key) { + continue; + } + g_ptr_array_add (priv->keys, key); + } + g_strfreev (custom_paths); + + keys_sync_queue (manager, TRUE, TRUE); + + gnome_settings_profile_end (NULL); +} + +static void +app_launched_cb (GAppLaunchContext *context, + GAppInfo *info, + GVariant *platform_data, + gpointer user_data) +{ + GsdMediaKeysManager *manager = GSD_MEDIA_KEYS_MANAGER (user_data); + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + gint32 pid; + const gchar *app_name; + + if (!g_variant_lookup (platform_data, "pid", "i", &pid)) + return; + + app_name = g_app_info_get_id (info); + if (app_name == NULL) + app_name = g_app_info_get_executable (info); + + /* Start async request; we don't care about the result */ + gnome_start_systemd_scope (app_name, + pid, + NULL, + priv->connection, + NULL, NULL, NULL); +} + +static void +launch_app (GsdMediaKeysManager *manager, + GAppInfo *app_info, + gint64 timestamp) +{ + GError *error = NULL; + GdkAppLaunchContext *launch_context; + + /* setup the launch context so the startup notification is correct */ + launch_context = gdk_display_get_app_launch_context (gdk_display_get_default ()); + gdk_app_launch_context_set_timestamp (launch_context, timestamp); + set_launch_context_env (manager, G_APP_LAUNCH_CONTEXT (launch_context)); + + g_signal_connect_object (launch_context, + "launched", + G_CALLBACK (app_launched_cb), + manager, + 0); + + if (!g_app_info_launch (app_info, NULL, G_APP_LAUNCH_CONTEXT (launch_context), &error)) { + g_warning ("Could not launch '%s': %s", + g_app_info_get_commandline (app_info), + error->message); + g_error_free (error); + } + g_object_unref (launch_context); +} + +static void +execute (GsdMediaKeysManager *manager, + char *cmd, + gint64 timestamp) +{ + GAppInfo *app_info; + g_autofree gchar *escaped = NULL; + gchar *p; + + /* Escape all % characters as g_app_info_create_from_commandline will + * try to interpret them otherwise. */ + escaped = g_malloc (strlen (cmd) * 2 + 1); + p = escaped; + while (*cmd) { + *p = *cmd; + p++; + if (*cmd == '%') { + *p = '%'; + p++; + } + cmd++; + } + *p = '\0'; + + app_info = g_app_info_create_from_commandline (escaped, NULL, G_APP_INFO_CREATE_NONE, NULL); + launch_app (manager, app_info, timestamp); + g_object_unref (app_info); +} + +static void +do_url_action (GsdMediaKeysManager *manager, + const char *scheme, + gint64 timestamp) +{ + GAppInfo *app_info; + + app_info = g_app_info_get_default_for_uri_scheme (scheme); + if (app_info != NULL) { + launch_app (manager, app_info, timestamp); + g_object_unref (app_info); + } else { + g_warning ("Could not find default application for '%s' scheme", scheme); + } +} + +static void +do_media_action (GsdMediaKeysManager *manager, + gint64 timestamp) +{ + GAppInfo *app_info; + + app_info = g_app_info_get_default_for_type ("audio/x-vorbis+ogg", FALSE); + if (app_info != NULL) { + launch_app (manager, app_info, timestamp); + g_object_unref (app_info); + } else { + g_warning ("Could not find default application for '%s' mime-type", "audio/x-vorbis+ogg"); + } +} + +static void +gnome_session_shutdown_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GVariant *result; + GError *error = NULL; + + result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), + res, + &error); + if (result == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to call Shutdown on session manager: %s", + error->message); + g_error_free (error); + } else { + g_variant_unref (result); + } +} + +static void +gnome_session_shutdown (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GDBusProxy *proxy; + + proxy = G_DBUS_PROXY (gnome_settings_bus_get_session_proxy ()); + + g_dbus_proxy_call (proxy, + "Shutdown", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + priv->bus_cancellable, + gnome_session_shutdown_cb, + NULL); + + g_object_unref (proxy); +} + +static void +do_eject_action_cb (GDrive *drive, + GAsyncResult *res, + GsdMediaKeysManager *manager) +{ + g_drive_eject_with_operation_finish (drive, res, NULL); +} + +#define NO_SCORE 0 +#define SCORE_CAN_EJECT 50 +#define SCORE_HAS_MEDIA 100 +static void +do_eject_action (GsdMediaKeysManager *manager) +{ + GList *drives, *l; + GDrive *fav_drive; + guint score; + GVolumeMonitor *volume_monitor; + + volume_monitor = g_volume_monitor_get (); + + + /* Find the best drive to eject */ + fav_drive = NULL; + score = NO_SCORE; + drives = g_volume_monitor_get_connected_drives (volume_monitor); + for (l = drives; l != NULL; l = l->next) { + GDrive *drive = l->data; + + if (g_drive_can_eject (drive) == FALSE) + continue; + if (g_drive_is_media_removable (drive) == FALSE) + continue; + if (score < SCORE_CAN_EJECT) { + fav_drive = drive; + score = SCORE_CAN_EJECT; + } + if (g_drive_has_media (drive) == FALSE) + continue; + if (score < SCORE_HAS_MEDIA) { + fav_drive = drive; + score = SCORE_HAS_MEDIA; + break; + } + } + + /* Show OSD */ + show_osd (manager, "media-eject-symbolic", NULL, -1, NULL); + + /* Clean up the drive selection and exit if no suitable + * drives are found */ + if (fav_drive != NULL) + fav_drive = g_object_ref (fav_drive); + + g_list_foreach (drives, (GFunc) g_object_unref, NULL); + if (fav_drive == NULL) + return; + + /* Eject! */ + g_drive_eject_with_operation (fav_drive, G_MOUNT_UNMOUNT_FORCE, + NULL, NULL, + (GAsyncReadyCallback) do_eject_action_cb, + manager); + g_object_unref (fav_drive); + g_object_unref (volume_monitor); +} + +static void +do_home_key_action (GsdMediaKeysManager *manager, + gint64 timestamp) +{ + GFile *file; + GError *error = NULL; + char *uri; + + file = g_file_new_for_path (g_get_home_dir ()); + uri = g_file_get_uri (file); + g_object_unref (file); + + if (gtk_show_uri_on_window (NULL, uri, timestamp, &error) == FALSE) { + g_warning ("Failed to launch '%s': %s", uri, error->message); + g_error_free (error); + } + g_free (uri); +} + +static void +do_search_action (GsdMediaKeysManager *manager, + gint64 timestamp) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->shell_proxy == NULL) + return; + + gsd_shell_call_focus_search (priv->shell_proxy, + NULL, NULL, NULL); +} + +static void +do_execute_desktop_or_desktop (GsdMediaKeysManager *manager, + const char *desktop, + const char *alt_desktop, + gint64 timestamp) +{ + GDesktopAppInfo *app_info; + + app_info = g_desktop_app_info_new (desktop); + if (app_info == NULL && alt_desktop != NULL) + app_info = g_desktop_app_info_new (alt_desktop); + + if (app_info != NULL) { + launch_app (manager, G_APP_INFO (app_info), timestamp); + g_object_unref (app_info); + return; + } + + g_warning ("Could not find application '%s' or '%s'", desktop, alt_desktop); +} + +static void +do_touchpad_osd_action (GsdMediaKeysManager *manager, gboolean state) +{ + show_osd (manager, state ? "input-touchpad-symbolic" + : "touchpad-disabled-symbolic", NULL, -1, NULL); +} + +static void +do_touchpad_action (GsdMediaKeysManager *manager) +{ + GSettings *settings; + gboolean state; + + settings = g_settings_new (SETTINGS_TOUCHPAD_DIR); + state = (g_settings_get_enum (settings, TOUCHPAD_ENABLED_KEY) == + G_DESKTOP_DEVICE_SEND_EVENTS_ENABLED); + + do_touchpad_osd_action (manager, !state); + + g_settings_set_enum (settings, TOUCHPAD_ENABLED_KEY, + !state ? + G_DESKTOP_DEVICE_SEND_EVENTS_ENABLED : + G_DESKTOP_DEVICE_SEND_EVENTS_DISABLED); + g_object_unref (settings); +} + +static void +on_screen_locked (GsdScreenSaver *screen_saver, + GAsyncResult *result, + GsdMediaKeysManager *manager) +{ + gboolean is_locked; + GError *error = NULL; + + is_locked = gsd_screen_saver_call_lock_finish (screen_saver, result, &error); + + if (!is_locked) { + g_warning ("Couldn't lock screen: %s", error->message); + g_error_free (error); + return; + } +} + +static void +do_lock_screensaver (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->screen_saver_proxy == NULL) + priv->screen_saver_proxy = gnome_settings_bus_get_screen_saver_proxy (); + + gsd_screen_saver_call_lock (priv->screen_saver_proxy, + priv->bus_cancellable, + (GAsyncReadyCallback) on_screen_locked, + manager); +} + +static void +sound_theme_changed (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + char *theme_name; + + g_object_get (G_OBJECT (priv->gtksettings), "gtk-sound-theme-name", &theme_name, NULL); + if (theme_name) + ca_context_change_props (priv->ca, CA_PROP_CANBERRA_XDG_THEME_NAME, theme_name, NULL); + g_free (theme_name); +} + +static void +allow_volume_above_100_percent_changed_cb (GSettings *settings, + const char *settings_key, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + gboolean allow_volume_above_100_percent; + + g_assert (g_str_equal (settings_key, ALLOW_VOLUME_ABOVE_100_PERCENT_KEY)); + + allow_volume_above_100_percent = g_settings_get_boolean (settings, settings_key); + priv->max_volume = allow_volume_above_100_percent ? PA_VOLUME_UI_MAX : PA_VOLUME_NORM; +} + +static void +play_volume_changed_audio (GsdMediaKeysManager *manager, + GvcMixerStream *stream) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->ca == NULL) { + ca_context_create (&priv->ca); + ca_context_set_driver (priv->ca, "pulse"); + ca_context_change_props (priv->ca, 0, + CA_PROP_APPLICATION_ID, + "org.gnome.VolumeControl", + NULL); + + priv->gtksettings = + gtk_settings_get_for_screen (gdk_screen_get_default ()); + + g_signal_connect_swapped (priv->gtksettings, + "notify::gtk-sound-theme-name", + G_CALLBACK (sound_theme_changed), + manager); + sound_theme_changed (manager); + } + + ca_context_change_device (priv->ca, + gvc_mixer_stream_get_name (stream)); + ca_context_play (priv->ca, 1, + CA_PROP_EVENT_ID, "audio-volume-change", + CA_PROP_EVENT_DESCRIPTION, "volume changed through key press", + CA_PROP_CANBERRA_CACHE_CONTROL, "permanent", + NULL); +} + +static void +show_volume_osd (GsdMediaKeysManager *manager, + GvcMixerStream *stream, + guint vol, + gboolean muted, + gboolean sound_changed, + gboolean quiet) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GvcMixerUIDevice *device; + const GvcMixerStreamPort *port; + const char *icon; + gboolean playing = FALSE; + double new_vol; + double max_volume; + + max_volume = (double) priv->max_volume / PA_VOLUME_NORM; + if (!muted) { + new_vol = (double) vol / PA_VOLUME_NORM; + new_vol = CLAMP (new_vol, 0, max_volume); + } else { + new_vol = 0.0; + } + icon = get_icon_name_for_volume (!GVC_IS_MIXER_SINK (stream), muted, new_vol); + port = gvc_mixer_stream_get_port (stream); + if (g_strcmp0 (gvc_mixer_stream_get_form_factor (stream), "internal") != 0 || + (port != NULL && + g_strcmp0 (port->port, "analog-output-speaker") != 0 && + g_strcmp0 (port->port, "analog-output") != 0)) { + device = gvc_mixer_control_lookup_device_from_stream (priv->volume, stream); + show_osd_with_max_level (manager, icon, + gvc_mixer_ui_device_get_description (device), + new_vol, max_volume, NULL); + } else { + show_osd_with_max_level (manager, icon, NULL, new_vol, max_volume, NULL); + } + + if (priv->ca) + ca_context_playing (priv->ca, 1, &playing); + playing = !playing && gvc_mixer_stream_get_state (stream) == GVC_STREAM_STATE_RUNNING; + + if (quiet == FALSE && sound_changed != FALSE && muted == FALSE && playing == FALSE) + play_volume_changed_audio (manager, stream); +} + +#if HAVE_GUDEV +/* PulseAudio gives us /devices/... paths, when udev + * expects /sys/devices/... paths. */ +static GUdevDevice * +get_udev_device_for_sysfs_path (GsdMediaKeysManager *manager, + const char *sysfs_path) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + char *path; + GUdevDevice *dev; + + path = g_strdup_printf ("/sys%s", sysfs_path); + dev = g_udev_client_query_by_sysfs_path (priv->udev_client, path); + g_free (path); + + return dev; +} + +static GvcMixerStream * +get_stream_for_device_node (GsdMediaKeysManager *manager, + gboolean is_output, + const gchar *devnode) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + gpointer id_ptr; + GvcMixerStream *res; + GUdevDevice *dev, *parent; + GSList *streams, *l; + + id_ptr = g_hash_table_lookup (priv->streams, devnode); + if (id_ptr != NULL) { + if (GPOINTER_TO_UINT (id_ptr) == (guint) -1) + return NULL; + else + return gvc_mixer_control_lookup_stream_id (priv->volume, GPOINTER_TO_UINT (id_ptr)); + } + + dev = g_udev_client_query_by_device_file (priv->udev_client, devnode); + if (dev == NULL) { + g_debug ("Could not find udev device for device path '%s'", devnode); + return NULL; + } + + if (g_strcmp0 (g_udev_device_get_property (dev, "ID_BUS"), "usb") != 0) { + g_debug ("Not handling XInput device %s, not USB", devnode); + g_hash_table_insert (priv->streams, + g_strdup (devnode), + GUINT_TO_POINTER ((guint) -1)); + g_object_unref (dev); + return NULL; + } + + parent = g_udev_device_get_parent_with_subsystem (dev, "usb", "usb_device"); + if (parent == NULL) { + g_warning ("No USB device parent for XInput device %s even though it's USB", devnode); + g_object_unref (dev); + return NULL; + } + + res = NULL; + if (is_output) + streams = gvc_mixer_control_get_sinks (priv->volume); + else + streams = gvc_mixer_control_get_sources (priv->volume); + for (l = streams; l; l = l->next) { + GvcMixerStream *stream = l->data; + const char *sysfs_path; + GUdevDevice *stream_dev, *stream_parent; + + sysfs_path = gvc_mixer_stream_get_sysfs_path (stream); + stream_dev = get_udev_device_for_sysfs_path (manager, sysfs_path); + if (stream_dev == NULL) + continue; + stream_parent = g_udev_device_get_parent_with_subsystem (stream_dev, "usb", "usb_device"); + g_object_unref (stream_dev); + if (stream_parent == NULL) + continue; + + if (g_strcmp0 (g_udev_device_get_sysfs_path (stream_parent), + g_udev_device_get_sysfs_path (parent)) == 0) { + res = stream; + } + g_object_unref (stream_parent); + if (res != NULL) + break; + } + + g_slist_free (streams); + + if (res) + g_hash_table_insert (priv->streams, + g_strdup (devnode), + GUINT_TO_POINTER (gvc_mixer_stream_get_id (res))); + else + g_hash_table_insert (priv->streams, + g_strdup (devnode), + GUINT_TO_POINTER ((guint) -1)); + + return res; +} +#endif /* HAVE_GUDEV */ + +typedef enum { + SOUND_ACTION_FLAG_IS_OUTPUT = 1 << 0, + SOUND_ACTION_FLAG_IS_QUIET = 1 << 1, + SOUND_ACTION_FLAG_IS_PRECISE = 1 << 2, +} SoundActionFlags; + +static void +do_sound_action (GsdMediaKeysManager *manager, + const gchar *device_node, + int type, + SoundActionFlags flags) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GvcMixerStream *stream = NULL; + gboolean old_muted, new_muted; + guint old_vol, new_vol, norm_vol_step, vol_step; + gboolean sound_changed; + + /* Find the stream that corresponds to the device, if any */ + stream = NULL; +#if HAVE_GUDEV + if (device_node) { + stream = get_stream_for_device_node (manager, + flags & SOUND_ACTION_FLAG_IS_OUTPUT, + device_node); + } +#endif /* HAVE_GUDEV */ + + if (stream == NULL) { + if (flags & SOUND_ACTION_FLAG_IS_OUTPUT) + stream = priv->sink; + else + stream = priv->source; + } + + if (stream == NULL) + return; + + if (flags & SOUND_ACTION_FLAG_IS_PRECISE) { + norm_vol_step = PA_VOLUME_NORM * VOLUME_STEP_PRECISE / 100; + } + else { + vol_step = g_settings_get_int (priv->settings, VOLUME_STEP); + norm_vol_step = PA_VOLUME_NORM * vol_step / 100; + } + /* FIXME: this is racy */ + new_vol = old_vol = gvc_mixer_stream_get_volume (stream); + new_muted = old_muted = gvc_mixer_stream_get_is_muted (stream); + sound_changed = FALSE; + + switch (type) { + case MUTE_KEY: + new_muted = !old_muted; + break; + case VOLUME_DOWN_KEY: + if (old_vol <= norm_vol_step) { + new_vol = 0; + new_muted = TRUE; + } else { + new_vol = old_vol - norm_vol_step; + } + break; + case VOLUME_UP_KEY: + new_muted = FALSE; + /* When coming out of mute only increase the volume if it was 0 */ + if (!old_muted || old_vol == 0) + new_vol = MIN (old_vol + norm_vol_step, priv->max_volume); + break; + } + + if (old_muted != new_muted) { + gvc_mixer_stream_change_is_muted (stream, new_muted); + sound_changed = TRUE; + } + + if (old_vol != new_vol) { + if (gvc_mixer_stream_set_volume (stream, new_vol) != FALSE) { + gvc_mixer_stream_push_volume (stream); + sound_changed = TRUE; + } + } + + show_volume_osd (manager, stream, new_vol, new_muted, sound_changed, + flags & SOUND_ACTION_FLAG_IS_QUIET); +} + +static void +update_default_sink (GsdMediaKeysManager *manager, + gboolean warn) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GvcMixerStream *stream; + + stream = gvc_mixer_control_get_default_sink (priv->volume); + if (stream == priv->sink) + return; + + g_clear_object (&priv->sink); + + if (stream != NULL) { + priv->sink = g_object_ref (stream); + } else { + if (warn) + g_warning ("Unable to get default sink"); + else + g_debug ("Unable to get default sink"); + } +} + +static void +update_default_source (GsdMediaKeysManager *manager, + gboolean warn) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GvcMixerStream *stream; + + stream = gvc_mixer_control_get_default_source (priv->volume); + if (stream == priv->source) + return; + + g_clear_object (&priv->source); + + if (stream != NULL) { + priv->source = g_object_ref (stream); + } else { + if (warn) + g_warning ("Unable to get default source"); + else + g_debug ("Unable to get default source"); + } +} + +static void +on_control_state_changed (GvcMixerControl *control, + GvcMixerControlState new_state, + GsdMediaKeysManager *manager) +{ + update_default_sink (manager, new_state == GVC_STATE_READY); + update_default_source (manager, new_state == GVC_STATE_READY); +} + +static void +on_control_default_sink_changed (GvcMixerControl *control, + guint id, + GsdMediaKeysManager *manager) +{ + update_default_sink (manager, TRUE); +} + +static void +on_control_default_source_changed (GvcMixerControl *control, + guint id, + GsdMediaKeysManager *manager) +{ + update_default_source (manager, TRUE); +} + +#if HAVE_GUDEV +static gboolean +remove_stream (gpointer key, + gpointer value, + gpointer id) +{ + if (GPOINTER_TO_UINT (value) == GPOINTER_TO_UINT (id)) + return TRUE; + return FALSE; +} +#endif /* HAVE_GUDEV */ + +static void +on_control_stream_removed (GvcMixerControl *control, + guint id, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->sink != NULL) { + if (gvc_mixer_stream_get_id (priv->sink) == id) + g_clear_object (&priv->sink); + } + if (priv->source != NULL) { + if (gvc_mixer_stream_get_id (priv->source) == id) + g_clear_object (&priv->source); + } + +#if HAVE_GUDEV + g_hash_table_foreach_remove (priv->streams, (GHRFunc) remove_stream, GUINT_TO_POINTER (id)); +#endif +} + +static void +free_media_player (MediaPlayer *player) +{ + if (player->watch_id > 0) { + g_bus_unwatch_name (player->watch_id); + player->watch_id = 0; + } + g_free (player->application); + g_free (player->dbus_name); + g_free (player); +} + +static gint +find_by_application (gconstpointer a, + gconstpointer b) +{ + return strcmp (((MediaPlayer *)a)->application, b); +} + +static gint +find_by_name (gconstpointer a, + gconstpointer b) +{ + return strcmp (((MediaPlayer *)a)->dbus_name, b); +} + +static gint +find_by_time (gconstpointer a, + gconstpointer b) +{ + return ((MediaPlayer *)a)->time < ((MediaPlayer *)b)->time; +} + +static void +name_vanished_handler (GDBusConnection *connection, + const gchar *name, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GList *iter; + + iter = g_list_find_custom (priv->media_players, + name, + find_by_name); + + if (iter != NULL) { + MediaPlayer *player; + + player = iter->data; + g_debug ("Deregistering vanished %s (dbus_name: %s)", player->application, player->dbus_name); + free_media_player (player); + priv->media_players = g_list_delete_link (priv->media_players, iter); + } +} + +/* + * Register a new media player. Most applications will want to call + * this with time = GDK_CURRENT_TIME. This way, the last registered + * player will receive media events. In some cases, applications + * may want to register with a lower priority (usually 1), to grab + * events only nobody else is interested in. + */ +static void +gsd_media_keys_manager_grab_media_player_keys (GsdMediaKeysManager *manager, + const char *application, + const char *dbus_name, + guint32 time) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GList *iter; + MediaPlayer *media_player; + guint watch_id; + + if (time == GDK_CURRENT_TIME) { + GTimeVal tv; + + g_get_current_time (&tv); + time = tv.tv_sec * 1000 + tv.tv_usec / 1000; + } + + iter = g_list_find_custom (priv->media_players, + application, + find_by_application); + + if (iter != NULL) { + if (((MediaPlayer *)iter->data)->time < time) { + MediaPlayer *player = iter->data; + free_media_player (player); + priv->media_players = g_list_delete_link (priv->media_players, iter); + } else { + return; + } + } + + watch_id = g_bus_watch_name (G_BUS_TYPE_SESSION, + dbus_name, + G_BUS_NAME_WATCHER_FLAGS_NONE, + NULL, + (GBusNameVanishedCallback) name_vanished_handler, + manager, + NULL); + + g_debug ("Registering %s at %u", application, time); + media_player = g_new0 (MediaPlayer, 1); + media_player->application = g_strdup (application); + media_player->dbus_name = g_strdup (dbus_name); + media_player->time = time; + media_player->watch_id = watch_id; + + priv->media_players = g_list_insert_sorted (priv->media_players, + media_player, + find_by_time); +} + +static void +gsd_media_keys_manager_release_media_player_keys (GsdMediaKeysManager *manager, + const char *application, + const char *name) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GList *iter = NULL; + + g_return_if_fail (application != NULL || name != NULL); + + if (application != NULL) { + iter = g_list_find_custom (priv->media_players, + application, + find_by_application); + } + + if (iter == NULL && name != NULL) { + iter = g_list_find_custom (priv->media_players, + name, + find_by_name); + } + + if (iter != NULL) { + MediaPlayer *player; + + player = iter->data; + g_debug ("Deregistering %s (dbus_name: %s)", application, player->dbus_name); + free_media_player (player); + priv->media_players = g_list_delete_link (priv->media_players, iter); + } +} + +static gboolean +gsd_media_player_key_pressed (GsdMediaKeysManager *manager, + const char *key) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + const char *application; + gboolean have_listeners; + GError *error = NULL; + MediaPlayer *player; + + g_return_val_if_fail (key != NULL, FALSE); + + g_debug ("Media key '%s' pressed", key); + + /* Prefer MPRIS players to those using the native API */ + if (mpris_controller_get_has_active_player (priv->mpris_controller)) { + if (mpris_controller_key (priv->mpris_controller, key)) + return TRUE; + } + + have_listeners = (priv->media_players != NULL); + + if (!have_listeners) { + /* Popup a dialog with an (/) icon */ + show_osd (manager, "action-unavailable-symbolic", NULL, -1, NULL); + return TRUE; + } + + player = priv->media_players->data; + application = player->application; + + if (g_dbus_connection_emit_signal (priv->connection, + player->dbus_name, + GSD_MEDIA_KEYS_DBUS_PATH, + GSD_MEDIA_KEYS_DBUS_NAME, + "MediaPlayerKeyPressed", + g_variant_new ("(ss)", application ? application : "", key), + &error) == FALSE) { + g_debug ("Error emitting signal: %s", error->message); + g_error_free (error); + } + + return !have_listeners; +} + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GsdMediaKeysManager *manager = (GsdMediaKeysManager *) user_data; + + g_debug ("Calling method '%s' for media-keys", method_name); + + if (g_strcmp0 (method_name, "ReleaseMediaPlayerKeys") == 0) { + const char *app_name; + + g_variant_get (parameters, "(&s)", &app_name); + gsd_media_keys_manager_release_media_player_keys (manager, app_name, sender); + g_dbus_method_invocation_return_value (invocation, NULL); + } else if (g_strcmp0 (method_name, "GrabMediaPlayerKeys") == 0) { + const char *app_name; + guint32 time; + + g_variant_get (parameters, "(&su)", &app_name, &time); + gsd_media_keys_manager_grab_media_player_keys (manager, app_name, sender, time); + g_dbus_method_invocation_return_value (invocation, NULL); + } +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + NULL, /* Get Property */ + NULL, /* Set Property */ +}; + +static gboolean +do_multimedia_player_action (GsdMediaKeysManager *manager, + const char *key) +{ + return gsd_media_player_key_pressed (manager, key); +} + +static void +sensor_properties_changed (GDBusProxy *proxy, + GVariant *changed_properties, + GStrv invalidated_properties, + gpointer user_data) +{ + GsdMediaKeysManager *manager = user_data; + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GVariant *v; + GVariantDict dict; + + if (priv->iio_sensor_proxy == NULL) + return; + + if (changed_properties) + g_variant_dict_init (&dict, changed_properties); + + if (changed_properties == NULL || + g_variant_dict_contains (&dict, "HasAccelerometer")) { + v = g_dbus_proxy_get_cached_property (priv->iio_sensor_proxy, + "HasAccelerometer"); + if (v == NULL) { + g_debug ("Couldn't fetch HasAccelerometer property"); + return; + } + priv->has_accel = g_variant_get_boolean (v); + g_variant_unref (v); + } +} + +static void +iio_sensor_appeared_cb (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer user_data) +{ + GsdMediaKeysManager *manager = user_data; + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GError *error = NULL; + + priv->iio_sensor_proxy = g_dbus_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "net.hadess.SensorProxy", + "/net/hadess/SensorProxy", + "net.hadess.SensorProxy", + NULL, + &error); + + if (priv->iio_sensor_proxy == NULL) { + g_warning ("Failed to access net.hadess.SensorProxy after it appeared"); + return; + } + g_signal_connect (G_OBJECT (priv->iio_sensor_proxy), + "g-properties-changed", + G_CALLBACK (sensor_properties_changed), manager); + + sensor_properties_changed (priv->iio_sensor_proxy, + NULL, NULL, manager); +} + +static void +iio_sensor_disappeared_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GsdMediaKeysManager *manager = user_data; + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + g_clear_object (&priv->iio_sensor_proxy); + priv->has_accel = FALSE; +} + +static void +do_video_rotate_lock_action (GsdMediaKeysManager *manager, + gint64 timestamp) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GSettings *settings; + gboolean locked; + + if (!priv->has_accel) { + g_debug ("Ignoring attempt to set orientation lock: no accelerometer"); + return; + } + + settings = g_settings_new ("org.gnome.settings-daemon.peripherals.touchscreen"); + locked = !g_settings_get_boolean (settings, "orientation-lock"); + g_settings_set_boolean (settings, "orientation-lock", locked); + g_object_unref (settings); + + show_osd (manager, locked ? "rotation-locked-symbolic" + : "rotation-allowed-symbolic", NULL, -1, NULL); +} + +static void +do_toggle_accessibility_key (const char *key) +{ + GSettings *settings; + gboolean state; + + settings = g_settings_new ("org.gnome.desktop.a11y.applications"); + state = g_settings_get_boolean (settings, key); + g_settings_set_boolean (settings, key, !state); + g_object_unref (settings); +} + +static void +do_magnifier_action (GsdMediaKeysManager *manager) +{ + do_toggle_accessibility_key ("screen-magnifier-enabled"); +} + +static void +do_screenreader_action (GsdMediaKeysManager *manager) +{ + do_toggle_accessibility_key ("screen-reader-enabled"); +} + +static void +do_on_screen_keyboard_action (GsdMediaKeysManager *manager) +{ + do_toggle_accessibility_key ("screen-keyboard-enabled"); +} + +static void +do_text_size_action (GsdMediaKeysManager *manager, + MediaKeyType type) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + gdouble factor, best, distance; + guint i; + + /* Same values used in the Seeing tab of the Universal Access panel */ + static gdouble factors[] = { + 0.75, + 1.0, + 1.25, + 1.5 + }; + + /* Figure out the current DPI scaling factor */ + factor = g_settings_get_double (priv->interface_settings, "text-scaling-factor"); + factor += (type == INCREASE_TEXT_KEY ? 0.25 : -0.25); + + /* Try to find a matching value */ + distance = 1e6; + best = 1.0; + for (i = 0; i < G_N_ELEMENTS(factors); i++) { + gdouble d; + d = fabs (factor - factors[i]); + if (d < distance) { + best = factors[i]; + distance = d; + } + } + + if (best == 1.0) + g_settings_reset (priv->interface_settings, "text-scaling-factor"); + else + g_settings_set_double (priv->interface_settings, "text-scaling-factor", best); +} + +static void +do_magnifier_zoom_action (GsdMediaKeysManager *manager, + MediaKeyType type) +{ + GSettings *settings; + gdouble offset, value; + + if (type == MAGNIFIER_ZOOM_IN_KEY) + offset = 1.0; + else + offset = -1.0; + + settings = g_settings_new ("org.gnome.desktop.a11y.magnifier"); + value = g_settings_get_double (settings, "mag-factor"); + value += offset; + value = roundl (value); + g_settings_set_double (settings, "mag-factor", value); + g_object_unref (settings); +} + +static void +do_toggle_contrast_action (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + gboolean high_contrast; + char *theme; + + /* Are we using HighContrast now? */ + theme = g_settings_get_string (priv->interface_settings, "gtk-theme"); + high_contrast = g_str_equal (theme, HIGH_CONTRAST); + g_free (theme); + + if (high_contrast != FALSE) { + if (priv->gtk_theme == NULL) + g_settings_reset (priv->interface_settings, "gtk-theme"); + else + g_settings_set (priv->interface_settings, "gtk-theme", priv->gtk_theme); + g_settings_set (priv->interface_settings, "icon-theme", priv->icon_theme); + } else { + g_settings_set (priv->interface_settings, "gtk-theme", HIGH_CONTRAST); + g_settings_set (priv->interface_settings, "icon-theme", HIGH_CONTRAST); + } +} + +static void +power_action (GsdMediaKeysManager *manager, + const char *action, + gboolean allow_interaction) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + g_dbus_proxy_call (priv->logind_proxy, + action, + g_variant_new ("(b)", allow_interaction), + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, + priv->bus_cancellable, + NULL, NULL); +} + +static void +do_config_power_action (GsdMediaKeysManager *manager, + GsdPowerActionType action_type, + gboolean in_lock_screen) +{ + switch (action_type) { + case GSD_POWER_ACTION_SUSPEND: + power_action (manager, "Suspend", !in_lock_screen); + break; + case GSD_POWER_ACTION_INTERACTIVE: + if (!in_lock_screen) + gnome_session_shutdown (manager); + break; + case GSD_POWER_ACTION_SHUTDOWN: + power_action (manager, "PowerOff", !in_lock_screen); + break; + case GSD_POWER_ACTION_HIBERNATE: + power_action (manager, "Hibernate", !in_lock_screen); + break; + case GSD_POWER_ACTION_BLANK: + case GSD_POWER_ACTION_LOGOUT: + case GSD_POWER_ACTION_NOTHING: + /* these actions cannot be handled by media-keys and + * are not used in this context */ + break; + } +} + +static gboolean +supports_power_action (GsdMediaKeysManager *manager, + GsdPowerActionType action_type) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + const char *method_name = NULL; + g_autoptr(GVariant) variant = NULL; + const char *reply; + gboolean result = FALSE; + + switch (action_type) { + case GSD_POWER_ACTION_SUSPEND: + method_name = "CanSuspend"; + break; + case GSD_POWER_ACTION_SHUTDOWN: + method_name = "CanPowerOff"; + break; + case GSD_POWER_ACTION_HIBERNATE: + method_name = "CanHibernate"; + break; + case GSD_POWER_ACTION_INTERACTIVE: + case GSD_POWER_ACTION_BLANK: + case GSD_POWER_ACTION_LOGOUT: + case GSD_POWER_ACTION_NOTHING: + break; + } + + if (method_name == NULL) + return FALSE; + + variant = g_dbus_proxy_call_sync (priv->logind_proxy, + method_name, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + priv->bus_cancellable, + NULL); + + if (variant == NULL) + return FALSE; + + g_variant_get (variant, "(&s)", &reply); + if (g_strcmp0 (reply, "yes") == 0) + result = TRUE; + + return result; +} + +static void +do_config_power_button_action (GsdMediaKeysManager *manager, + gboolean in_lock_screen) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GsdPowerButtonActionType action_type; + GsdPowerActionType action; + + if (priv->power_button_disabled) + return; + + /* Always power off VMs when power off is pressed in the menus */ + if (g_strcmp0 (priv->chassis_type, "vm") == 0) { + power_action (manager, "PowerOff", !in_lock_screen); + return; + } + + action_type = g_settings_get_enum (priv->power_settings, "power-button-action"); + switch (action_type) { + case GSD_POWER_BUTTON_ACTION_SUSPEND: + action = GSD_POWER_ACTION_SUSPEND; + break; + case GSD_POWER_BUTTON_ACTION_HIBERNATE: + action = GSD_POWER_ACTION_HIBERNATE; + break; + case GSD_POWER_BUTTON_ACTION_INTERACTIVE: + action = GSD_POWER_ACTION_INTERACTIVE; + break; + case GSD_POWER_BUTTON_ACTION_NOTHING: + /* do nothing */ + return; + } + + if (action != GSD_POWER_ACTION_INTERACTIVE && !supports_power_action (manager, action)) + action = GSD_POWER_ACTION_INTERACTIVE; + + do_config_power_action (manager, action, in_lock_screen); +} + +static void +update_brightness_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GError *error = NULL; + int percentage; + GVariant *variant; + GsdMediaKeysManager *manager = GSD_MEDIA_KEYS_MANAGER (user_data); + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + const char *icon, *debug; + char *connector = NULL; + + /* update the dialog with the new value */ + if (G_DBUS_PROXY (source_object) == priv->power_keyboard_proxy) { + debug = "keyboard"; + } else { + debug = "screen"; + } + + variant = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), + res, &error); + if (variant == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to set new %s percentage: %s", + debug, error->message); + g_error_free (error); + return; + } + + /* update the dialog with the new value */ + if (G_DBUS_PROXY (source_object) == priv->power_keyboard_proxy) { + icon = "keyboard-brightness-symbolic"; + g_variant_get (variant, "(i)", &percentage); + } else { + icon = "display-brightness-symbolic"; + g_variant_get (variant, "(i&s)", &percentage, &connector); + } + + show_osd (manager, icon, NULL, (double) percentage / 100.0, connector); + g_variant_unref (variant); +} + +static void +do_brightness_action (GsdMediaKeysManager *manager, + MediaKeyType type) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + const char *cmd; + GDBusProxy *proxy; + + switch (type) { + case KEYBOARD_BRIGHTNESS_UP_KEY: + case KEYBOARD_BRIGHTNESS_DOWN_KEY: + case KEYBOARD_BRIGHTNESS_TOGGLE_KEY: + proxy = priv->power_keyboard_proxy; + break; + case SCREEN_BRIGHTNESS_UP_KEY: + case SCREEN_BRIGHTNESS_DOWN_KEY: + case SCREEN_BRIGHTNESS_CYCLE_KEY: + proxy = priv->power_screen_proxy; + break; + default: + g_assert_not_reached (); + } + + if (priv->connection == NULL || + proxy == NULL) { + g_warning ("No existing D-Bus connection trying to handle power keys"); + return; + } + + switch (type) { + case KEYBOARD_BRIGHTNESS_UP_KEY: + case SCREEN_BRIGHTNESS_UP_KEY: + cmd = "StepUp"; + break; + case KEYBOARD_BRIGHTNESS_DOWN_KEY: + case SCREEN_BRIGHTNESS_DOWN_KEY: + cmd = "StepDown"; + break; + case KEYBOARD_BRIGHTNESS_TOGGLE_KEY: + cmd = "Toggle"; + break; + case SCREEN_BRIGHTNESS_CYCLE_KEY: + cmd = "Cycle"; + break; + default: + g_assert_not_reached (); + } + + /* call into the power plugin */ + g_dbus_proxy_call (proxy, + cmd, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + update_brightness_cb, + manager); +} + +static void +do_battery_action (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + gdouble percentage; + UpDeviceKind kind; + gchar *icon_name; + + g_return_if_fail (priv->composite_device != NULL); + + g_object_get (priv->composite_device, + "kind", &kind, + "icon-name", &icon_name, + "percentage", &percentage, + NULL); + + if (kind == UP_DEVICE_KIND_UPS || kind == UP_DEVICE_KIND_BATTERY) { + g_debug ("showing battery level OSD"); + show_osd (manager, icon_name, NULL, (double) percentage / 100.0, NULL); + } + + g_free (icon_name); +} + +static gboolean +get_rfkill_property (GsdMediaKeysManager *manager, + const char *property) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GVariant *v; + gboolean ret; + + v = g_dbus_proxy_get_cached_property (priv->rfkill_proxy, property); + if (!v) + return FALSE; + ret = g_variant_get_boolean (v); + g_variant_unref (v); + + return ret; +} + +typedef struct { + GsdMediaKeysManager *manager; + char *property; + gboolean bluetooth; + gboolean target_state; +} RfkillData; + +static void +set_rfkill_complete (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GError *error = NULL; + GVariant *variant; + RfkillData *data = user_data; + + variant = g_dbus_proxy_call_finish (G_DBUS_PROXY (object), result, &error); + + if (variant == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to set '%s' property: %s", data->property, error->message); + g_error_free (error); + goto out; + } + g_variant_unref (variant); + + g_debug ("Finished changing rfkill, property %s is now %s", + data->property, data->target_state ? "true" : "false"); + + if (data->bluetooth) { + if (data->target_state) + show_osd (data->manager, "bluetooth-disabled-symbolic", + _("Bluetooth disabled"), -1, NULL); + else + show_osd (data->manager, "bluetooth-active-symbolic", + _("Bluetooth enabled"), -1, NULL); + } else { + if (data->target_state) + show_osd (data->manager, "airplane-mode-symbolic", + _("Airplane mode enabled"), -1, NULL); + else + show_osd (data->manager, "network-wireless-signal-excellent-symbolic", + _("Airplane mode disabled"), -1, NULL); + } + +out: + g_free (data->property); + g_free (data); +} + +static void +do_rfkill_action (GsdMediaKeysManager *manager, + gboolean bluetooth) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + const char *has_mode, *hw_mode, *mode; + gboolean new_state; + RfkillData *data; + + has_mode = bluetooth ? "BluetoothHasAirplaneMode" : "HasAirplaneMode"; + hw_mode = bluetooth ? "BluetoothHardwareAirplaneMode" : "HardwareAirplaneMode"; + mode = bluetooth ? "BluetoothAirplaneMode" : "AirplaneMode"; + + if (priv->rfkill_proxy == NULL) + return; + + if (get_rfkill_property (manager, has_mode) == FALSE) + return; + + if (get_rfkill_property (manager, hw_mode)) { + show_osd (manager, "airplane-mode-symbolic", + _("Hardware Airplane Mode"), -1, NULL); + return; + } + + new_state = !get_rfkill_property (manager, mode); + data = g_new0 (RfkillData, 1); + data->manager = manager; + data->property = g_strdup (mode); + data->bluetooth = bluetooth; + data->target_state = new_state; + g_dbus_proxy_call (priv->rfkill_proxy, + "org.freedesktop.DBus.Properties.Set", + g_variant_new ("(ssv)", + "org.gnome.SettingsDaemon.Rfkill", + data->property, + g_variant_new_boolean (new_state)), + G_DBUS_CALL_FLAGS_NONE, -1, + priv->rfkill_cancellable, + set_rfkill_complete, data); + + g_debug ("Setting rfkill property %s to %s", + data->property, new_state ? "true" : "false"); +} + +static void +screencast_stop (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->screencast_timeout_id > 0) { + g_source_remove (priv->screencast_timeout_id); + priv->screencast_timeout_id = 0; + } + + g_dbus_proxy_call (priv->screencast_proxy, + "StopScreencast", NULL, + G_DBUS_CALL_FLAGS_NONE, -1, + priv->screencast_cancellable, + NULL, NULL); + + priv->screencast_recording = FALSE; +} + +static gboolean +screencast_timeout (gpointer user_data) +{ + GsdMediaKeysManager *manager = user_data; + screencast_stop (manager); + return G_SOURCE_REMOVE; +} + +static void +screencast_start (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + guint max_length; + g_dbus_proxy_call (priv->screencast_proxy, + "Screencast", + g_variant_new_parsed ("(%s, @a{sv} {})", + /* Translators: this is a filename used for screencast + * recording, where "%d" and "%t" date and time, e.g. + * "Screencast from 07-17-2013 10:00:46 PM.webm" */ + /* xgettext:no-c-format */ + _("Screencast from %d %t.webm")), + G_DBUS_CALL_FLAGS_NONE, -1, + priv->screencast_cancellable, + NULL, NULL); + + max_length = g_settings_get_uint (priv->settings, "max-screencast-length"); + + if (max_length > 0) { + priv->screencast_timeout_id = g_timeout_add_seconds (max_length, + screencast_timeout, + manager); + g_source_set_name_by_id (priv->screencast_timeout_id, "[gnome-settings-daemon] screencast_timeout"); + } + priv->screencast_recording = TRUE; +} + +static void +do_screencast_action (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->screencast_proxy == NULL) + return; + + if (!priv->screencast_recording) + screencast_start (manager); + else + screencast_stop (manager); +} + +static void +do_custom_action (GsdMediaKeysManager *manager, + const gchar *device_node, + MediaKey *key, + gint64 timestamp) +{ + g_debug ("Launching custom action for key (on device node %s)", device_node); + + execute (manager, key->custom_command, timestamp); +} + +static gboolean +do_action (GsdMediaKeysManager *manager, + const gchar *device_node, + guint mode, + MediaKeyType type, + gint64 timestamp) +{ + g_debug ("Launching action for key type '%d' (on device node %s)", type, device_node); + + gboolean power_action_noninteractive = (POWER_KEYS_MODE_NO_DIALOG & mode); + + switch (type) { + case TOUCHPAD_KEY: + do_touchpad_action (manager); + break; + case TOUCHPAD_ON_KEY: + do_touchpad_osd_action (manager, TRUE); + break; + case TOUCHPAD_OFF_KEY: + do_touchpad_osd_action (manager, FALSE); + break; + case MUTE_KEY: + case VOLUME_DOWN_KEY: + case VOLUME_UP_KEY: + do_sound_action (manager, device_node, type, SOUND_ACTION_FLAG_IS_OUTPUT); + break; + case MIC_MUTE_KEY: + do_sound_action (manager, device_node, MUTE_KEY, SOUND_ACTION_FLAG_IS_QUIET); + break; + case MUTE_QUIET_KEY: + do_sound_action (manager, device_node, MUTE_KEY, + SOUND_ACTION_FLAG_IS_OUTPUT | SOUND_ACTION_FLAG_IS_QUIET); + break; + case VOLUME_DOWN_QUIET_KEY: + do_sound_action (manager, device_node, VOLUME_DOWN_KEY, + SOUND_ACTION_FLAG_IS_OUTPUT | SOUND_ACTION_FLAG_IS_QUIET); + break; + case VOLUME_UP_QUIET_KEY: + do_sound_action (manager, device_node, VOLUME_UP_KEY, + SOUND_ACTION_FLAG_IS_OUTPUT | SOUND_ACTION_FLAG_IS_QUIET); + break; + case VOLUME_DOWN_PRECISE_KEY: + do_sound_action (manager, device_node, VOLUME_DOWN_KEY, + SOUND_ACTION_FLAG_IS_OUTPUT | SOUND_ACTION_FLAG_IS_PRECISE); + break; + case VOLUME_UP_PRECISE_KEY: + do_sound_action (manager, device_node, VOLUME_UP_KEY, + SOUND_ACTION_FLAG_IS_OUTPUT | SOUND_ACTION_FLAG_IS_PRECISE); + break; + case LOGOUT_KEY: + gnome_session_shutdown (manager); + break; + case EJECT_KEY: + do_eject_action (manager); + break; + case HOME_KEY: + do_home_key_action (manager, timestamp); + break; + case SEARCH_KEY: + do_search_action (manager, timestamp); + break; + case EMAIL_KEY: + do_url_action (manager, "mailto", timestamp); + break; + case SCREENSAVER_KEY: + do_lock_screensaver (manager); + break; + case HELP_KEY: + do_url_action (manager, "ghelp", timestamp); + break; + case SCREENSHOT_KEY: + case SCREENSHOT_CLIP_KEY: + case WINDOW_SCREENSHOT_KEY: + case WINDOW_SCREENSHOT_CLIP_KEY: + case AREA_SCREENSHOT_KEY: + case AREA_SCREENSHOT_CLIP_KEY: + gsd_screenshot_take (type); + break; + case SCREENCAST_KEY: + do_screencast_action (manager); + break; + case WWW_KEY: + do_url_action (manager, "http", timestamp); + break; + case MEDIA_KEY: + do_media_action (manager, timestamp); + break; + case CALCULATOR_KEY: + do_execute_desktop_or_desktop (manager, "org.gnome.Calculator.desktop", "gnome-calculator.desktop", timestamp); + break; + case CONTROL_CENTER_KEY: + do_execute_desktop_or_desktop (manager, "gnome-control-center.desktop", NULL, timestamp); + break; + case PLAY_KEY: + return do_multimedia_player_action (manager, "Play"); + case PAUSE_KEY: + return do_multimedia_player_action (manager, "Pause"); + case STOP_KEY: + return do_multimedia_player_action (manager, "Stop"); + case PREVIOUS_KEY: + return do_multimedia_player_action (manager, "Previous"); + case NEXT_KEY: + return do_multimedia_player_action (manager, "Next"); + case REWIND_KEY: + return do_multimedia_player_action (manager, "Rewind"); + case FORWARD_KEY: + return do_multimedia_player_action (manager, "FastForward"); + case REPEAT_KEY: + return do_multimedia_player_action (manager, "Repeat"); + case RANDOM_KEY: + return do_multimedia_player_action (manager, "Shuffle"); + case ROTATE_VIDEO_LOCK_KEY: + do_video_rotate_lock_action (manager, timestamp); + break; + case MAGNIFIER_KEY: + do_magnifier_action (manager); + break; + case SCREENREADER_KEY: + do_screenreader_action (manager); + break; + case ON_SCREEN_KEYBOARD_KEY: + do_on_screen_keyboard_action (manager); + break; + case INCREASE_TEXT_KEY: + case DECREASE_TEXT_KEY: + do_text_size_action (manager, type); + break; + case MAGNIFIER_ZOOM_IN_KEY: + case MAGNIFIER_ZOOM_OUT_KEY: + do_magnifier_zoom_action (manager, type); + break; + case TOGGLE_CONTRAST_KEY: + do_toggle_contrast_action (manager); + break; + case POWER_KEY: + do_config_power_button_action (manager, power_action_noninteractive); + break; + case SUSPEND_KEY: + do_config_power_action (manager, GSD_POWER_ACTION_SUSPEND, power_action_noninteractive); + break; + case HIBERNATE_KEY: + do_config_power_action (manager, GSD_POWER_ACTION_HIBERNATE, power_action_noninteractive); + break; + case SCREEN_BRIGHTNESS_UP_KEY: + case SCREEN_BRIGHTNESS_DOWN_KEY: + case SCREEN_BRIGHTNESS_CYCLE_KEY: + case KEYBOARD_BRIGHTNESS_UP_KEY: + case KEYBOARD_BRIGHTNESS_DOWN_KEY: + case KEYBOARD_BRIGHTNESS_TOGGLE_KEY: + do_brightness_action (manager, type); + break; + case BATTERY_KEY: + do_battery_action (manager); + break; + case RFKILL_KEY: + do_rfkill_action (manager, FALSE); + break; + case BLUETOOTH_RFKILL_KEY: + do_rfkill_action (manager, TRUE); + break; + /* Note, no default so compiler catches missing keys */ + case CUSTOM_KEY: + g_assert_not_reached (); + } + + return FALSE; +} + +static void +on_accelerator_activated (ShellKeyGrabber *grabber, + guint accel_id, + GVariant *parameters, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GVariantDict dict; + guint i; + guint deviceid; + gchar *device_node; + guint timestamp; + guint mode; + + g_variant_dict_init (&dict, parameters); + + if (!g_variant_dict_lookup (&dict, "device-id", "u", &deviceid)) + deviceid = 0; + if (!g_variant_dict_lookup (&dict, "device-node", "s", &device_node)) + device_node = NULL; + if (!g_variant_dict_lookup (&dict, "timestamp", "u", ×tamp)) + timestamp = GDK_CURRENT_TIME; + if (!g_variant_dict_lookup (&dict, "action-mode", "u", &mode)) + mode = 0; + + if (!device_node && !gnome_settings_is_wayland ()) + device_node = xdevice_get_device_node (deviceid); + + g_debug ("Received accel id %u (device-id: %u, timestamp: %u, mode: 0x%X)", + accel_id, deviceid, timestamp, mode); + + for (i = 0; i < priv->keys->len; i++) { + MediaKey *key; + guint j; + + key = g_ptr_array_index (priv->keys, i); + + for (j = 0; j < key->accel_ids->len; j++) { + if (g_array_index (key->accel_ids, guint, j) == accel_id) + break; + } + if (j >= key->accel_ids->len) + continue; + + if (key->key_type == CUSTOM_KEY) + do_custom_action (manager, device_node, key, timestamp); + else + do_action (manager, device_node, mode, key->key_type, timestamp); + + g_free (device_node); + return; + } + + g_warning ("Could not find accelerator for accel id %u", accel_id); + g_free (device_node); +} + +static void +update_theme_settings (GSettings *settings, + const char *key, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + char *theme; + + theme = g_settings_get_string (priv->interface_settings, key); + if (g_str_equal (theme, HIGH_CONTRAST)) { + g_free (theme); + } else { + if (g_str_equal (key, "gtk-theme")) { + g_free (priv->gtk_theme); + priv->gtk_theme = theme; + } else { + g_free (priv->icon_theme); + priv->icon_theme = theme; + } + } +} + +typedef struct { + GvcHeadsetPortChoice choice; + gchar *name; +} AudioSelectionChoice; + +static AudioSelectionChoice audio_selection_choices[] = { + { GVC_HEADSET_PORT_CHOICE_HEADPHONES, "headphones" }, + { GVC_HEADSET_PORT_CHOICE_HEADSET, "headset" }, + { GVC_HEADSET_PORT_CHOICE_MIC, "microphone" }, +}; + +static void +audio_selection_done (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer data) +{ + GsdMediaKeysManager *manager = GSD_MEDIA_KEYS_MANAGER (data); + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + const gchar *choice; + guint i; + + if (!priv->audio_selection_requested) + return; + + choice = NULL; + g_variant_get_child (parameters, 0, "&s", &choice); + if (!choice) + return; + + for (i = 0; i < G_N_ELEMENTS (audio_selection_choices); ++i) { + if (g_str_equal (choice, audio_selection_choices[i].name)) { + gvc_mixer_control_set_headset_port (priv->volume, + priv->audio_selection_device_id, + audio_selection_choices[i].choice); + break; + } + } + + priv->audio_selection_requested = FALSE; +} + +static void +audio_selection_needed (GvcMixerControl *control, + guint id, + gboolean show_dialog, + GvcHeadsetPortChoice choices, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + gchar *args[G_N_ELEMENTS (audio_selection_choices) + 1]; + guint i, n; + + if (!priv->audio_selection_conn) + return; + + if (priv->audio_selection_requested) { + g_dbus_connection_call (priv->audio_selection_conn, + AUDIO_SELECTION_DBUS_NAME, + AUDIO_SELECTION_DBUS_PATH, + AUDIO_SELECTION_DBUS_INTERFACE, + "Close", NULL, NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL, NULL); + priv->audio_selection_requested = FALSE; + } + + if (!show_dialog) + return; + + n = 0; + for (i = 0; i < G_N_ELEMENTS (audio_selection_choices); ++i) { + if (choices & audio_selection_choices[i].choice) + args[n++] = audio_selection_choices[i].name; + } + args[n] = NULL; + + priv->audio_selection_requested = TRUE; + priv->audio_selection_device_id = id; + g_dbus_connection_call (priv->audio_selection_conn, + AUDIO_SELECTION_DBUS_NAME, + AUDIO_SELECTION_DBUS_PATH, + AUDIO_SELECTION_DBUS_INTERFACE, + "Open", + g_variant_new ("(^as)", args), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL, NULL); +} + +static void +audio_selection_appeared (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer data) +{ + GsdMediaKeysManager *manager = data; + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + priv->audio_selection_conn = connection; + priv->audio_selection_signal_id = + g_dbus_connection_signal_subscribe (connection, + AUDIO_SELECTION_DBUS_NAME, + AUDIO_SELECTION_DBUS_INTERFACE, + "DeviceSelected", + AUDIO_SELECTION_DBUS_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + audio_selection_done, + manager, + NULL); +} + +static void +clear_audio_selection (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->audio_selection_signal_id) + g_dbus_connection_signal_unsubscribe (priv->audio_selection_conn, + priv->audio_selection_signal_id); + priv->audio_selection_signal_id = 0; + priv->audio_selection_conn = NULL; +} + +static void +audio_selection_vanished (GDBusConnection *connection, + const gchar *name, + gpointer data) +{ + if (connection) + clear_audio_selection (data); +} + +static void +initialize_volume_handler (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + /* initialise Volume handler + * + * We do this one here to force checking gstreamer cache, etc. + * The rest (grabbing and setting the keys) can happen in an + * idle. + */ + gnome_settings_profile_start ("gvc_mixer_control_new"); + + priv->volume = gvc_mixer_control_new ("GNOME Volume Control Media Keys"); + + g_signal_connect (priv->volume, + "state-changed", + G_CALLBACK (on_control_state_changed), + manager); + g_signal_connect (priv->volume, + "default-sink-changed", + G_CALLBACK (on_control_default_sink_changed), + manager); + g_signal_connect (priv->volume, + "default-source-changed", + G_CALLBACK (on_control_default_source_changed), + manager); + g_signal_connect (priv->volume, + "stream-removed", + G_CALLBACK (on_control_stream_removed), + manager); + g_signal_connect (priv->volume, + "audio-device-selection-needed", + G_CALLBACK (audio_selection_needed), + manager); + + gvc_mixer_control_open (priv->volume); + + priv->audio_selection_watch_id = + g_bus_watch_name (G_BUS_TYPE_SESSION, + AUDIO_SELECTION_DBUS_NAME, + G_BUS_NAME_WATCHER_FLAGS_NONE, + audio_selection_appeared, + audio_selection_vanished, + manager, + NULL); + + gnome_settings_profile_end ("gvc_mixer_control_new"); +} + +static void +on_screencast_proxy_ready (GObject *source, + GAsyncResult *result, + gpointer data) +{ + GsdMediaKeysManager *manager = data; + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GError *error = NULL; + + priv->screencast_proxy = + g_dbus_proxy_new_for_bus_finish (result, &error); + + if (!priv->screencast_proxy) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to create proxy for screencast: %s", error->message); + g_error_free (error); + } +} + +static void +on_key_grabber_ready (GObject *source, + GAsyncResult *result, + gpointer data) +{ + GsdMediaKeysManager *manager = data; + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GError *error = NULL; + + priv->key_grabber = shell_key_grabber_proxy_new_for_bus_finish (result, &error); + + if (!priv->key_grabber) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to create proxy for key grabber: %s", error->message); + g_error_free (error); + return; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (priv->key_grabber), + SHELL_GRABBER_CALL_TIMEOUT); + + g_signal_connect (priv->key_grabber, "accelerator-activated", + G_CALLBACK (on_accelerator_activated), manager); + + init_kbd (manager); +} + +static void +shell_presence_changed (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + gchar *name_owner; + + name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (priv->shell_proxy)); + + g_ptr_array_set_size (priv->keys, 0); + g_clear_object (&priv->key_grabber); + g_clear_object (&priv->screencast_proxy); + + if (name_owner) { + shell_key_grabber_proxy_new_for_bus (G_BUS_TYPE_SESSION, + 0, + name_owner, + SHELL_DBUS_PATH, + priv->grab_cancellable, + on_key_grabber_ready, manager); + g_free (name_owner); + } +} + +static void +on_rfkill_proxy_ready (GObject *source, + GAsyncResult *result, + gpointer data) +{ + GsdMediaKeysManager *manager = data; + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + priv->rfkill_proxy = + g_dbus_proxy_new_for_bus_finish (result, NULL); +} + +static void +rfkill_appeared_cb (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer user_data) +{ + GsdMediaKeysManager *manager = user_data; + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SESSION, + 0, NULL, + "org.gnome.SettingsDaemon.Rfkill", + "/org/gnome/SettingsDaemon/Rfkill", + "org.gnome.SettingsDaemon.Rfkill", + priv->rfkill_cancellable, + on_rfkill_proxy_ready, manager); +} + +static gboolean +start_media_keys_idle_cb (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + g_debug ("Starting media_keys manager"); + gnome_settings_profile_start (NULL); + + priv->keys = g_ptr_array_new_with_free_func ((GDestroyNotify) media_key_unref); + priv->keys_to_sync = g_hash_table_new_full (g_direct_hash, g_direct_equal, (GDestroyNotify) media_key_unref, NULL); + + initialize_volume_handler (manager); + + priv->settings = g_settings_new (SETTINGS_BINDING_DIR); + g_signal_connect (G_OBJECT (priv->settings), "changed", + G_CALLBACK (gsettings_changed_cb), manager); + g_signal_connect (G_OBJECT (priv->settings), "changed::custom-keybindings", + G_CALLBACK (gsettings_custom_changed_cb), manager); + + priv->custom_settings = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + + priv->sound_settings = g_settings_new (SETTINGS_SOUND_DIR); + g_signal_connect (G_OBJECT (priv->sound_settings), + "changed::" ALLOW_VOLUME_ABOVE_100_PERCENT_KEY, + G_CALLBACK (allow_volume_above_100_percent_changed_cb), manager); + allow_volume_above_100_percent_changed_cb (priv->sound_settings, + ALLOW_VOLUME_ABOVE_100_PERCENT_KEY, manager); + + /* for the power plugin interface code */ + priv->power_settings = g_settings_new (SETTINGS_POWER_DIR); + priv->chassis_type = gnome_settings_get_chassis_type (); + + /* Logic from http://git.gnome.org/browse/gnome-shell/tree/js/ui/status/accessibility.js#n163 */ + priv->interface_settings = g_settings_new (SETTINGS_INTERFACE_DIR); + g_signal_connect (G_OBJECT (priv->interface_settings), "changed::gtk-theme", + G_CALLBACK (update_theme_settings), manager); + g_signal_connect (G_OBJECT (priv->interface_settings), "changed::icon-theme", + G_CALLBACK (update_theme_settings), manager); + priv->gtk_theme = g_settings_get_string (priv->interface_settings, "gtk-theme"); + if (g_str_equal (priv->gtk_theme, HIGH_CONTRAST)) { + g_free (priv->gtk_theme); + priv->gtk_theme = NULL; + } + priv->icon_theme = g_settings_get_string (priv->interface_settings, "icon-theme"); + + priv->grab_cancellable = g_cancellable_new (); + priv->screencast_cancellable = g_cancellable_new (); + priv->rfkill_cancellable = g_cancellable_new (); + + priv->shell_proxy = gnome_settings_bus_get_shell_proxy (); + g_signal_connect_swapped (priv->shell_proxy, "notify::g-name-owner", + G_CALLBACK (shell_presence_changed), manager); + shell_presence_changed (manager); + + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SESSION, + 0, NULL, + SHELL_DBUS_NAME ".Screencast", + SHELL_DBUS_PATH "/Screencast", + SHELL_DBUS_NAME ".Screencast", + priv->screencast_cancellable, + on_screencast_proxy_ready, manager); + + priv->rfkill_watch_id = g_bus_watch_name (G_BUS_TYPE_SESSION, + "org.gnome.SettingsDaemon.Rfkill", + G_BUS_NAME_WATCHER_FLAGS_NONE, + rfkill_appeared_cb, + NULL, + manager, NULL); + + g_debug ("Starting mpris controller"); + priv->mpris_controller = mpris_controller_new (); + + /* Rotation */ + priv->iio_sensor_watch_id = g_bus_watch_name (G_BUS_TYPE_SYSTEM, + "net.hadess.SensorProxy", + G_BUS_NAME_WATCHER_FLAGS_NONE, + iio_sensor_appeared_cb, + iio_sensor_disappeared_cb, + manager, NULL); + + gnome_settings_profile_end (NULL); + + priv->start_idle_id = 0; + + return FALSE; +} + +static GVariant * +map_keybinding (GVariant *variant, GVariant *old_default, GVariant *new_default) +{ + g_autoptr(GPtrArray) array = g_ptr_array_new (); + g_autofree const gchar **defaults = NULL; + const gchar *old_default_value; + const gchar **pos; + const gchar *value; + + defaults = g_variant_get_strv (new_default, NULL); + g_return_val_if_fail (defaults != NULL, NULL); + pos = defaults; + + value = g_variant_get_string (variant, NULL); + old_default_value = g_variant_get_string (old_default, NULL); + + /* Reset the keybinding configuration even if the user has the default + * configured explicitly (as the key will be bound by the corresponding + * static binding now). */ + if (g_strcmp0 (value, old_default_value) == 0) + return NULL; + + /* If the user has a custom value that is not in the list, then + * insert it instead of the first default entry. */ + if (!g_strv_contains (defaults, value)) { + g_ptr_array_add (array, (gpointer) value); + if (*pos) + pos++; + } + + /* Add all remaining default values */ + for (; *pos; pos++) + g_ptr_array_add (array, (gpointer) *pos); + + g_ptr_array_add (array, NULL); + + return g_variant_new_strv ((const gchar * const *) array->pdata, -1); +} + +static void +migrate_keybinding_settings (void) +{ + GsdSettingsMigrateEntry binding_entries[] = { + { "calculator", "calculator", map_keybinding }, + { "control-center", "control-center", map_keybinding }, + { "email", "email", map_keybinding }, + { "eject", "eject", map_keybinding }, + { "help", "help", map_keybinding }, + { "home", "home", map_keybinding }, + { "media", "media", map_keybinding }, + { "next", "next", map_keybinding }, + { "pause", "pause", map_keybinding }, + { "play", "play", map_keybinding }, + { "logout", "logout", map_keybinding }, + { "previous", "previous", map_keybinding }, + { "screensaver", "screensaver", map_keybinding }, + { "search", "search", map_keybinding }, + { "stop", "stop", map_keybinding }, + { "volume-down", "volume-down", map_keybinding }, + { "volume-mute", "volume-mute", map_keybinding }, + { "volume-up", "volume-up", map_keybinding }, + { "mic-mute", "mic-mute", map_keybinding }, + { "screenshot", "screenshot", map_keybinding }, + { "window-screenshot", "window-screenshot", map_keybinding }, + { "area-screenshot", "area-screenshot", map_keybinding }, + { "screenshot-clip", "screenshot-clip", map_keybinding }, + { "window-screenshot-clip", "window-screenshot-clip", map_keybinding }, + { "area-screenshot-clip", "area-screenshot-clip", map_keybinding }, + { "screencast", "screencast", map_keybinding }, + { "www", "www", map_keybinding }, + { "magnifier", "magnifier", map_keybinding }, + { "screenreader", "screenreader", map_keybinding }, + { "on-screen-keyboard", "on-screen-keyboard", map_keybinding }, + { "increase-text-size", "increase-text-size", map_keybinding }, + { "decrease-text-size", "decrease-text-size", map_keybinding }, + { "toggle-contrast", "toggle-contrast", map_keybinding }, + { "magnifier-zoom-in", "magnifier-zoom-in", map_keybinding }, + { "magnifier-zoom-out", "magnifier-zoom-out", map_keybinding }, + }; + + gsd_settings_migrate_check ("org.gnome.settings-daemon.plugins.media-keys.deprecated", + "/org/gnome/settings-daemon/plugins/media-keys/", + "org.gnome.settings-daemon.plugins.media-keys", + "/org/gnome/settings-daemon/plugins/media-keys/", + binding_entries, G_N_ELEMENTS (binding_entries)); +} + +gboolean +gsd_media_keys_manager_start (GsdMediaKeysManager *manager, + GError **error) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + const char * const subsystems[] = { "input", "usb", "sound", NULL }; + + gnome_settings_profile_start (NULL); + + migrate_keybinding_settings (); + +#if HAVE_GUDEV + priv->streams = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + priv->udev_client = g_udev_client_new (subsystems); +#endif + + priv->start_idle_id = g_idle_add ((GSourceFunc) start_media_keys_idle_cb, manager); + g_source_set_name_by_id (priv->start_idle_id, "[gnome-settings-daemon] start_media_keys_idle_cb"); + + register_manager (manager_object); + + gnome_settings_profile_end (NULL); + + return TRUE; +} + +void +gsd_media_keys_manager_stop (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + g_debug ("Stopping media_keys manager"); + + if (priv->start_idle_id != 0) { + g_source_remove (priv->start_idle_id); + priv->start_idle_id = 0; + } + + if (priv->mmkeys_name_id > 0) { + g_bus_unown_name (priv->mmkeys_name_id); + priv->mmkeys_name_id = 0; + } + + if (priv->bus_cancellable != NULL) { + g_cancellable_cancel (priv->bus_cancellable); + g_object_unref (priv->bus_cancellable); + priv->bus_cancellable = NULL; + } + + if (priv->gtksettings != NULL) { + g_signal_handlers_disconnect_by_func (priv->gtksettings, sound_theme_changed, manager); + priv->gtksettings = NULL; + } + + if (priv->rfkill_watch_id > 0) { + g_bus_unwatch_name (priv->rfkill_watch_id); + priv->rfkill_watch_id = 0; + } + + if (priv->iio_sensor_watch_id > 0) { + g_bus_unwatch_name (priv->iio_sensor_watch_id); + priv->iio_sensor_watch_id = 0; + } + + if (priv->inhibit_suspend_fd != -1) { + close (priv->inhibit_suspend_fd); + priv->inhibit_suspend_fd = -1; + priv->inhibit_suspend_taken = FALSE; + } + + if (priv->reenable_power_button_timer_id) { + g_source_remove (priv->reenable_power_button_timer_id); + priv->reenable_power_button_timer_id = 0; + } + + g_clear_pointer (&priv->ca, ca_context_destroy); + +#if HAVE_GUDEV + g_clear_pointer (&priv->streams, g_hash_table_destroy); + g_clear_object (&priv->udev_client); +#endif /* HAVE_GUDEV */ + + g_clear_object (&priv->logind_proxy); + g_clear_object (&priv->settings); + g_clear_object (&priv->sound_settings); + g_clear_object (&priv->power_settings); + g_clear_object (&priv->power_proxy); + g_clear_object (&priv->power_screen_proxy); + g_clear_object (&priv->power_keyboard_proxy); + g_clear_object (&priv->composite_device); + g_clear_object (&priv->mpris_controller); + g_clear_object (&priv->screencast_proxy); + g_clear_object (&priv->iio_sensor_proxy); + g_clear_pointer (&priv->chassis_type, g_free); + + g_clear_pointer (&priv->introspection_data, g_dbus_node_info_unref); + g_clear_object (&priv->connection); + + if (priv->keys_sync_data) { + /* Cancel ongoing sync. */ + priv->keys_sync_data->cancelled = TRUE; + priv->keys_sync_data = NULL; + } + if (priv->keys_sync_source_id) + g_source_remove (priv->keys_sync_source_id); + priv->keys_sync_source_id = 0; + + /* Remove all grabs; i.e.: + * - add all keys to the sync queue + * - remove all keys from the internal keys list + * - call the function to start a sync + * - "cancel" the sync operation as the manager will be gone + */ + if (priv->keys != NULL) { + while (priv->keys->len) { + MediaKey *key = g_ptr_array_index (priv->keys, 0); + g_hash_table_add (priv->keys_to_sync, media_key_ref (key)); + g_ptr_array_remove_index_fast (priv->keys, 0); + } + + keys_sync_start (manager); + + g_clear_pointer (&priv->keys, g_ptr_array_unref); + } + + g_clear_pointer (&priv->keys_to_sync, g_hash_table_destroy); + + g_clear_object (&priv->key_grabber); + + if (priv->grab_cancellable != NULL) { + g_cancellable_cancel (priv->grab_cancellable); + g_clear_object (&priv->grab_cancellable); + } + + if (priv->screencast_cancellable != NULL) { + g_cancellable_cancel (priv->screencast_cancellable); + g_clear_object (&priv->screencast_cancellable); + } + + if (priv->rfkill_cancellable != NULL) { + g_cancellable_cancel (priv->rfkill_cancellable); + g_clear_object (&priv->rfkill_cancellable); + } + + g_clear_object (&priv->sink); + g_clear_object (&priv->source); + g_clear_object (&priv->volume); + + if (priv->media_players != NULL) { + g_list_free_full (priv->media_players, (GDestroyNotify) free_media_player); + priv->media_players = NULL; + } + + g_clear_object (&priv->shell_proxy); + + if (priv->audio_selection_watch_id) + g_bus_unwatch_name (priv->audio_selection_watch_id); + priv->audio_selection_watch_id = 0; + clear_audio_selection (manager); +} + +static void +inhibit_suspend_done (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GDBusProxy *proxy = G_DBUS_PROXY (source); + GsdMediaKeysManager *manager = GSD_MEDIA_KEYS_MANAGER (user_data); + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GError *error = NULL; + GVariant *res; + GUnixFDList *fd_list = NULL; + gint idx; + + res = g_dbus_proxy_call_with_unix_fd_list_finish (proxy, &fd_list, result, &error); + if (res == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Unable to inhibit suspend: %s", error->message); + g_error_free (error); + } else { + g_variant_get (res, "(h)", &idx); + priv->inhibit_suspend_fd = g_unix_fd_list_get (fd_list, idx, &error); + if (priv->inhibit_suspend_fd == -1) { + g_warning ("Failed to receive system suspend inhibitor fd: %s", error->message); + g_error_free (error); + } + g_debug ("System suspend inhibitor fd is %d", priv->inhibit_suspend_fd); + g_object_unref (fd_list); + g_variant_unref (res); + } +} + +/* We take a delay inhibitor here, which causes logind to send a PrepareForSleep + * signal, so that we can set power_button_disabled on suspend. + */ +static void +inhibit_suspend (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->inhibit_suspend_taken) { + g_debug ("already inhibited suspend"); + return; + } + g_debug ("Adding suspend delay inhibitor"); + priv->inhibit_suspend_taken = TRUE; + g_dbus_proxy_call_with_unix_fd_list (priv->logind_proxy, + "Inhibit", + g_variant_new ("(ssss)", + "sleep", + g_get_user_name (), + "GNOME handling keypresses", + "delay"), + 0, + G_MAXINT, + NULL, + NULL, + inhibit_suspend_done, + manager); +} + +static void +uninhibit_suspend (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->inhibit_suspend_fd == -1) { + g_debug ("no suspend delay inhibitor"); + return; + } + g_debug ("Removing suspend delay inhibitor"); + close (priv->inhibit_suspend_fd); + priv->inhibit_suspend_fd = -1; + priv->inhibit_suspend_taken = FALSE; +} + +static gboolean +reenable_power_button_timer_cb (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + priv->power_button_disabled = FALSE; + /* This is a one shot timer. */ + priv->reenable_power_button_timer_id = 0; + return G_SOURCE_REMOVE; +} + +static void +setup_reenable_power_button_timer (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->reenable_power_button_timer_id != 0) + return; + + priv->reenable_power_button_timer_id = + g_timeout_add (GSD_REENABLE_POWER_BUTTON_DELAY, + (GSourceFunc) reenable_power_button_timer_cb, + manager); + g_source_set_name_by_id (priv->reenable_power_button_timer_id, + "[GsdMediaKeysManager] Reenable power button timer"); +} + +static void +stop_reenable_power_button_timer (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + if (priv->reenable_power_button_timer_id == 0) + return; + + g_source_remove (priv->reenable_power_button_timer_id); + priv->reenable_power_button_timer_id = 0; +} + +static void +logind_proxy_signal_cb (GDBusProxy *proxy, + const gchar *sender_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GsdMediaKeysManager *manager = GSD_MEDIA_KEYS_MANAGER (user_data); + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + gboolean is_about_to_suspend; + + if (g_strcmp0 (signal_name, "PrepareForSleep") != 0) + return; + g_variant_get (parameters, "(b)", &is_about_to_suspend); + if (is_about_to_suspend) { + /* Some devices send a power-button press on resume when woken + * up with the power-button, suppress this, to avoid immediate + * re-suspend. */ + stop_reenable_power_button_timer (manager); + priv->power_button_disabled = TRUE; + uninhibit_suspend (manager); + } else { + inhibit_suspend (manager); + /* Re-enable power-button handling (after a small delay) */ + setup_reenable_power_button_timer (manager); + } +} + +static void +gsd_media_keys_manager_class_init (GsdMediaKeysManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_media_keys_manager_finalize; +} + +static void +inhibit_done (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GDBusProxy *proxy = G_DBUS_PROXY (source); + GsdMediaKeysManager *manager = GSD_MEDIA_KEYS_MANAGER (user_data); + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GError *error = NULL; + GVariant *res; + GUnixFDList *fd_list = NULL; + gint idx; + + res = g_dbus_proxy_call_with_unix_fd_list_finish (proxy, &fd_list, result, &error); + if (res == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Unable to inhibit keypresses: %s", error->message); + g_error_free (error); + } else { + g_variant_get (res, "(h)", &idx); + priv->inhibit_keys_fd = g_unix_fd_list_get (fd_list, idx, &error); + if (priv->inhibit_keys_fd == -1) { + g_warning ("Failed to receive system inhibitor fd: %s", error->message); + g_error_free (error); + } + g_debug ("System inhibitor fd is %d", priv->inhibit_keys_fd); + g_object_unref (fd_list); + g_variant_unref (res); + } +} + +static void +gsd_media_keys_manager_init (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GError *error; + GDBusConnection *bus; + + error = NULL; + priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error); + if (bus == NULL) { + g_warning ("Failed to connect to system bus: %s", + error->message); + g_error_free (error); + return; + } + + priv->logind_proxy = + g_dbus_proxy_new_sync (bus, + 0, + NULL, + SYSTEMD_DBUS_NAME, + SYSTEMD_DBUS_PATH, + SYSTEMD_DBUS_INTERFACE, + NULL, + &error); + + if (priv->logind_proxy == NULL) { + g_warning ("Failed to connect to systemd: %s", + error->message); + g_error_free (error); + } + + g_object_unref (bus); + + g_debug ("Adding system inhibitors for power keys"); + priv->inhibit_keys_fd = -1; + g_dbus_proxy_call_with_unix_fd_list (priv->logind_proxy, + "Inhibit", + g_variant_new ("(ssss)", + "handle-power-key:handle-suspend-key:handle-hibernate-key", + g_get_user_name (), + "GNOME handling keypresses", + "block"), + 0, + G_MAXINT, + NULL, + NULL, + inhibit_done, + manager); + + g_debug ("Adding delay inhibitor for suspend"); + priv->inhibit_suspend_fd = -1; + g_signal_connect (priv->logind_proxy, "g-signal", + G_CALLBACK (logind_proxy_signal_cb), + manager); + inhibit_suspend (manager); +} + +static void +gsd_media_keys_manager_finalize (GObject *object) +{ + GsdMediaKeysManager *manager = GSD_MEDIA_KEYS_MANAGER (object); + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + gsd_media_keys_manager_stop (manager); + + if (priv->inhibit_keys_fd != -1) + close (priv->inhibit_keys_fd); + + g_clear_object (&priv->logind_proxy); + g_clear_object (&priv->screen_saver_proxy); + + G_OBJECT_CLASS (gsd_media_keys_manager_parent_class)->finalize (object); +} + +static void +power_keyboard_proxy_signal_cb (GDBusProxy *proxy, + const gchar *sender_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GsdMediaKeysManager *manager = GSD_MEDIA_KEYS_MANAGER (user_data); + gint brightness; + const gchar *source; + + if (g_strcmp0 (signal_name, "BrightnessChanged") != 0) + return; + + g_variant_get (parameters, "(i&s)", &brightness, &source); + + /* For non "internal" changes we already show the osd when handling + * the hotkey causing the change. */ + if (g_strcmp0 (source, "internal") != 0) + return; + + show_osd (manager, "keyboard-brightness-symbolic", NULL, (double) brightness / 100.0, NULL); +} + +static void +power_ready_cb (GObject *source_object, + GAsyncResult *res, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GError *error = NULL; + + priv->power_proxy = g_dbus_proxy_new_finish (res, &error); + if (priv->power_proxy == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to get proxy for power: %s", + error->message); + g_error_free (error); + } +} + +static void +power_screen_ready_cb (GObject *source_object, + GAsyncResult *res, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GError *error = NULL; + + priv->power_screen_proxy = g_dbus_proxy_new_finish (res, &error); + if (priv->power_screen_proxy == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to get proxy for power (screen): %s", + error->message); + g_error_free (error); + } +} + +static void +power_keyboard_ready_cb (GObject *source_object, + GAsyncResult *res, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GError *error = NULL; + + priv->power_keyboard_proxy = g_dbus_proxy_new_finish (res, &error); + if (priv->power_keyboard_proxy == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to get proxy for power (keyboard): %s", + error->message); + g_error_free (error); + } + + g_signal_connect (priv->power_keyboard_proxy, "g-signal", + G_CALLBACK (power_keyboard_proxy_signal_cb), + manager); +} + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *res, + GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + GDBusConnection *connection; + GError *error = NULL; + UpClient *up_client; + + connection = g_bus_get_finish (res, &error); + if (connection == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Could not get session bus: %s", error->message); + g_error_free (error); + return; + } + priv->connection = connection; + + g_dbus_connection_register_object (connection, + GSD_MEDIA_KEYS_DBUS_PATH, + priv->introspection_data->interfaces[0], + &interface_vtable, + manager, + NULL, + NULL); + + priv->mmkeys_name_id = g_bus_own_name_on_connection (priv->connection, + "org.gnome.SettingsDaemon.MediaKeys", + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, NULL, NULL, NULL); + + g_dbus_proxy_new (priv->connection, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + GSD_DBUS_NAME ".Power", + GSD_DBUS_PATH "/Power", + GSD_DBUS_BASE_INTERFACE ".Power", + NULL, + (GAsyncReadyCallback) power_ready_cb, + manager); + + g_dbus_proxy_new (priv->connection, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + GSD_DBUS_NAME ".Power", + GSD_DBUS_PATH "/Power", + GSD_DBUS_BASE_INTERFACE ".Power.Screen", + NULL, + (GAsyncReadyCallback) power_screen_ready_cb, + manager); + + g_dbus_proxy_new (priv->connection, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + GSD_DBUS_NAME ".Power", + GSD_DBUS_PATH "/Power", + GSD_DBUS_BASE_INTERFACE ".Power.Keyboard", + NULL, + (GAsyncReadyCallback) power_keyboard_ready_cb, + manager); + + up_client = up_client_new (); + priv->composite_device = up_client_get_display_device (up_client); + g_object_unref (up_client); +} + +static void +register_manager (GsdMediaKeysManager *manager) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + priv->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + priv->bus_cancellable = g_cancellable_new (); + g_assert (priv->introspection_data != NULL); + + g_bus_get (G_BUS_TYPE_SESSION, + priv->bus_cancellable, + (GAsyncReadyCallback) on_bus_gotten, + manager); +} + +GsdMediaKeysManager * +gsd_media_keys_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_MEDIA_KEYS_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_MEDIA_KEYS_MANAGER (manager_object); +} diff --git a/plugins/media-keys/gsd-media-keys-manager.h b/plugins/media-keys/gsd-media-keys-manager.h new file mode 100644 index 0000000..d7042b8 --- /dev/null +++ b/plugins/media-keys/gsd-media-keys-manager.h @@ -0,0 +1,47 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_MEDIA_KEYS_MANAGER_H +#define __GSD_MEDIA_KEYS_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_MEDIA_KEYS_MANAGER (gsd_media_keys_manager_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsdMediaKeysManager, gsd_media_keys_manager, GSD, MEDIA_KEYS_MANAGER, GObject) + +struct _GsdMediaKeysManagerClass +{ + GObjectClass parent_class; + void (* media_player_key_pressed) (GsdMediaKeysManager *manager, + const char *application, + const char *key); +}; + +GsdMediaKeysManager * gsd_media_keys_manager_new (void); +gboolean gsd_media_keys_manager_start (GsdMediaKeysManager *manager, + GError **error); +void gsd_media_keys_manager_stop (GsdMediaKeysManager *manager); + + +G_END_DECLS + +#endif /* __GSD_MEDIA_KEYS_MANAGER_H */ diff --git a/plugins/media-keys/gsd-screenshot-utils.c b/plugins/media-keys/gsd-screenshot-utils.c new file mode 100644 index 0000000..17ea32d --- /dev/null +++ b/plugins/media-keys/gsd-screenshot-utils.c @@ -0,0 +1,310 @@ +/* gsd-screenshot-utils.c - utilities to take screenshots + * + * Copyright (C) 2012 Red Hat, Inc. + * + * Adapted from gnome-screenshot code, which is + * Copyright (C) 2001-2006 Jonathan Blandford <jrb@alum.mit.edu> + * Copyright (C) 2006 Emmanuele Bassi <ebassi@gnome.org> + * Copyright (C) 2008-2012 Cosimo Cecchi <cosimoc@gnome.org> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + */ + +#include <config.h> +#include <canberra-gtk.h> +#include <gtk/gtk.h> +#include <gio/gio.h> +#include <glib/gi18n.h> +#include <string.h> +#include <glib/gstdio.h> + +#include "gsd-screenshot-utils.h" + +#define SHELL_SCREENSHOT_BUS_NAME "org.gnome.Shell" +#define SHELL_SCREENSHOT_BUS_PATH "/org/gnome/Shell/Screenshot" +#define SHELL_SCREENSHOT_BUS_IFACE "org.gnome.Shell.Screenshot" + +typedef enum { + SCREENSHOT_TYPE_SCREEN, + SCREENSHOT_TYPE_WINDOW, + SCREENSHOT_TYPE_AREA +} ScreenshotType; + +typedef struct { + ScreenshotType type; + gboolean copy_to_clipboard; + + GdkRectangle area_selection; + gchar *save_filename; + gchar *used_filename; + + GDBusConnection *connection; +} ScreenshotContext; + +static void +screenshot_play_sound_effect (const gchar *event_id, + const gchar *event_desc) +{ + ca_context *c; + + c = ca_gtk_context_get (); + ca_context_play (c, 0, + CA_PROP_EVENT_ID, event_id, + CA_PROP_EVENT_DESCRIPTION, event_desc, + CA_PROP_CANBERRA_CACHE_CONTROL, "permanent", + NULL); +} + +static void +screenshot_context_free (ScreenshotContext *ctx) +{ + g_free (ctx->save_filename); + g_free (ctx->used_filename); + g_clear_object (&ctx->connection); + g_slice_free (ScreenshotContext, ctx); +} + +static void +screenshot_play_error_sound_effect (void) +{ + screenshot_play_sound_effect ("dialog-error", _("Unable to capture a screenshot")); +} + +static void +screenshot_save_to_recent (ScreenshotContext *ctx) +{ + GFile *file = g_file_new_for_path (ctx->used_filename); + gchar *uri = g_file_get_uri (file); + + gtk_recent_manager_add_item (gtk_recent_manager_get_default (), uri); + + g_free (uri); + g_object_unref (file); +} + +static void +bus_call_ready_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GError *error = NULL; + ScreenshotContext *ctx = user_data; + GVariant *variant; + gboolean success; + + variant = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), res, &error); + + if (error != NULL) + { + screenshot_play_error_sound_effect (); + g_warning ("Failed to save a screenshot: %s\n", error->message); + g_error_free (error); + screenshot_context_free (ctx); + + return; + } + + g_variant_get (variant, "(bs)", &success, &ctx->used_filename); + + if (success) + { + if (!ctx->copy_to_clipboard) + { + screenshot_play_sound_effect ("screen-capture", _("Screenshot taken")); + screenshot_save_to_recent (ctx); + } + } + + screenshot_context_free (ctx); + g_variant_unref (variant); +} + +static void +screenshot_call_shell (ScreenshotContext *ctx) +{ + const gchar *method_name; + GVariant *method_params; + + if (ctx->type == SCREENSHOT_TYPE_SCREEN) + { + method_name = "Screenshot"; + method_params = g_variant_new ("(bbs)", + FALSE, /* include pointer */ + TRUE, /* flash */ + ctx->save_filename); + } + else if (ctx->type == SCREENSHOT_TYPE_WINDOW) + { + method_name = "ScreenshotWindow"; + method_params = g_variant_new ("(bbbs)", + TRUE, /* include border */ + FALSE, /* include pointer */ + TRUE, /* flash */ + ctx->save_filename); + } + else + { + method_name = "ScreenshotArea"; + method_params = g_variant_new ("(iiiibs)", + ctx->area_selection.x, ctx->area_selection.y, + ctx->area_selection.width, ctx->area_selection.height, + TRUE, /* flash */ + ctx->save_filename); + } + + g_dbus_connection_call (ctx->connection, + SHELL_SCREENSHOT_BUS_NAME, + SHELL_SCREENSHOT_BUS_PATH, + SHELL_SCREENSHOT_BUS_IFACE, + method_name, + method_params, + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + NULL, + bus_call_ready_cb, + ctx); +} + +static void +area_selection_ready_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GdkRectangle rectangle; + ScreenshotContext *ctx = user_data; + GVariant *geometry; + + geometry = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), + res, NULL); + + /* cancelled by the user */ + if (!geometry) + { + screenshot_context_free (ctx); + return; + } + + g_variant_get (geometry, "(iiii)", + &rectangle.x, &rectangle.y, + &rectangle.width, &rectangle.height); + + ctx->area_selection = rectangle; + screenshot_call_shell (ctx); + g_variant_unref (geometry); +} + +static void +bus_connection_ready_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GError *error = NULL; + ScreenshotContext *ctx = user_data; + + ctx->connection = g_bus_get_finish (res, &error); + + if (error != NULL) + { + screenshot_play_error_sound_effect (); + g_warning ("Failed to save a screenshot: %s\n", error->message); + g_error_free (error); + screenshot_context_free (ctx); + + return; + } + + if (ctx->type == SCREENSHOT_TYPE_AREA) + g_dbus_connection_call (ctx->connection, + SHELL_SCREENSHOT_BUS_NAME, + SHELL_SCREENSHOT_BUS_PATH, + SHELL_SCREENSHOT_BUS_IFACE, + "SelectArea", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + NULL, + area_selection_ready_cb, + ctx); + else + screenshot_call_shell (ctx); +} + +static void +screenshot_take (ScreenshotContext *ctx) +{ + g_bus_get (G_BUS_TYPE_SESSION, NULL, bus_connection_ready_cb, ctx); +} + +static gchar * +screenshot_build_filename (void) +{ + char *file_name, *origin; + GDateTime *d; + + d = g_date_time_new_now_local (); + origin = g_date_time_format (d, "%Y-%m-%d %H-%M-%S"); + g_date_time_unref (d); + + /* translators: this is the name of the file that gets made up + * with the screenshot */ + file_name = g_strdup_printf (_("Screenshot from %s"), origin); + g_free (origin); + + return file_name; +} + +static void +screenshot_check_name_ready (ScreenshotContext *ctx) +{ + if (ctx->copy_to_clipboard) + ctx->save_filename = g_strdup (""); + else + ctx->save_filename = screenshot_build_filename (); + + screenshot_take (ctx); +} + +void +gsd_screenshot_take (MediaKeyType key_type) +{ + ScreenshotContext *ctx = g_slice_new0 (ScreenshotContext); + + ctx->copy_to_clipboard = (key_type == SCREENSHOT_CLIP_KEY || + key_type == WINDOW_SCREENSHOT_CLIP_KEY || + key_type == AREA_SCREENSHOT_CLIP_KEY); + + switch (key_type) + { + case SCREENSHOT_KEY: + case SCREENSHOT_CLIP_KEY: + ctx->type = SCREENSHOT_TYPE_SCREEN; + break; + case WINDOW_SCREENSHOT_KEY: + case WINDOW_SCREENSHOT_CLIP_KEY: + ctx->type = SCREENSHOT_TYPE_WINDOW; + break; + case AREA_SCREENSHOT_KEY: + case AREA_SCREENSHOT_CLIP_KEY: + ctx->type = SCREENSHOT_TYPE_AREA; + break; + default: + g_assert_not_reached (); + break; + } + + screenshot_check_name_ready (ctx); +} diff --git a/plugins/media-keys/gsd-screenshot-utils.h b/plugins/media-keys/gsd-screenshot-utils.h new file mode 100644 index 0000000..39ae136 --- /dev/null +++ b/plugins/media-keys/gsd-screenshot-utils.h @@ -0,0 +1,36 @@ +/* gsd-screenshot-utils.h - utilities to take screenshots + * + * Copyright (C) 2012 Red Hat, Inc. + * + * Adapted from gnome-screenshot code, which is + * Copyright (C) 2001-2006 Jonathan Blandford <jrb@alum.mit.edu> + * Copyright (C) 2006 Emmanuele Bassi <ebassi@gnome.org> + * Copyright (C) 2008-2012 Cosimo Cecchi <cosimoc@gnome.org> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + */ + +#ifndef __GSD_SCREENSHOT_UTILS_H__ +#define __GSD_SCREENSHOT_UTILS_H__ + +#include "media-keys.h" + +G_BEGIN_DECLS + +void gsd_screenshot_take (MediaKeyType key_type); + +G_END_DECLS + +#endif /* __GSD_SCREENSHOT_UTILS_H__ */ diff --git a/plugins/media-keys/main.c b/plugins/media-keys/main.c new file mode 100644 index 0000000..fab28f6 --- /dev/null +++ b/plugins/media-keys/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_media_keys_manager_new +#define START gsd_media_keys_manager_start +#define STOP gsd_media_keys_manager_stop +#define MANAGER GsdMediaKeysManager +#include "gsd-media-keys-manager.h" + +#include "daemon-skeleton-gtk.h" diff --git a/plugins/media-keys/media-keys.h b/plugins/media-keys/media-keys.h new file mode 100644 index 0000000..cc4ea8e --- /dev/null +++ b/plugins/media-keys/media-keys.h @@ -0,0 +1,86 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2001 Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __MEDIA_KEYS_H__ +#define __MEDIA_KEYS_H__ + +typedef enum { + TOUCHPAD_KEY, + TOUCHPAD_ON_KEY, + TOUCHPAD_OFF_KEY, + MUTE_KEY, + VOLUME_DOWN_KEY, + VOLUME_UP_KEY, + MIC_MUTE_KEY, + MUTE_QUIET_KEY, + VOLUME_DOWN_QUIET_KEY, + VOLUME_UP_QUIET_KEY, + VOLUME_DOWN_PRECISE_KEY, + VOLUME_UP_PRECISE_KEY, + LOGOUT_KEY, + EJECT_KEY, + HOME_KEY, + MEDIA_KEY, + CALCULATOR_KEY, + SEARCH_KEY, + EMAIL_KEY, + CONTROL_CENTER_KEY, + SCREENSAVER_KEY, + HELP_KEY, + SCREENSHOT_KEY, + WINDOW_SCREENSHOT_KEY, + AREA_SCREENSHOT_KEY, + SCREENSHOT_CLIP_KEY, + WINDOW_SCREENSHOT_CLIP_KEY, + AREA_SCREENSHOT_CLIP_KEY, + SCREENCAST_KEY, + WWW_KEY, + PLAY_KEY, + PAUSE_KEY, + STOP_KEY, + PREVIOUS_KEY, + NEXT_KEY, + REWIND_KEY, + FORWARD_KEY, + REPEAT_KEY, + RANDOM_KEY, + ROTATE_VIDEO_LOCK_KEY, + MAGNIFIER_KEY, + SCREENREADER_KEY, + ON_SCREEN_KEYBOARD_KEY, + INCREASE_TEXT_KEY, + DECREASE_TEXT_KEY, + TOGGLE_CONTRAST_KEY, + MAGNIFIER_ZOOM_IN_KEY, + MAGNIFIER_ZOOM_OUT_KEY, + POWER_KEY, + SUSPEND_KEY, + HIBERNATE_KEY, + SCREEN_BRIGHTNESS_UP_KEY, + SCREEN_BRIGHTNESS_DOWN_KEY, + SCREEN_BRIGHTNESS_CYCLE_KEY, + KEYBOARD_BRIGHTNESS_UP_KEY, + KEYBOARD_BRIGHTNESS_DOWN_KEY, + KEYBOARD_BRIGHTNESS_TOGGLE_KEY, + BATTERY_KEY, + RFKILL_KEY, + BLUETOOTH_RFKILL_KEY, + CUSTOM_KEY +} MediaKeyType; + +#endif /* __MEDIA_KEYS_H__ */ diff --git a/plugins/media-keys/meson.build b/plugins/media-keys/meson.build new file mode 100644 index 0000000..cbda8ee --- /dev/null +++ b/plugins/media-keys/meson.build @@ -0,0 +1,59 @@ +sources = files( + 'bus-watch-namespace.c', + 'gsd-media-keys-manager.c', + 'gsd-screenshot-utils.c', + 'main.c', + 'mpris-controller.c' +) + +marshal = 'gsd-marshal' + +sources += gnome.genmarshal( + marshal, + sources: marshal + '.list', + prefix: marshal.underscorify(), + internal: true +) + +sources += gnome.gdbus_codegen( + 'shell-key-grabber', + 'org.gnome.ShellKeyGrabber.xml', + interface_prefix: 'org.gnome.', + namespace: 'Shell' +) + +deps = plugins_deps + [ + gio_unix_dep, + gsettings_desktop_dep, + libcanberra_gtk_dep, + libcommon_dep, + gnome_desktop_dep, + libgvc_dep, + libpulse_mainloop_glib_dep, + m_dep, + upower_glib_dep +] + +if enable_gudev + deps += gudev_dep +endif + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, data_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) + +program = 'audio-selection-test' + +executable( + program, + program + '.c', + include_directories: top_inc, + dependencies: deps +) diff --git a/plugins/media-keys/mpris-controller.c b/plugins/media-keys/mpris-controller.c new file mode 100644 index 0000000..c70f0bc --- /dev/null +++ b/plugins/media-keys/mpris-controller.c @@ -0,0 +1,326 @@ +/* + * Copyright © 2013 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, see <http://www.gnu.org/licenses> + * + * Author: Michael Wood <michael.g.wood@intel.com> + */ + +#include "mpris-controller.h" +#include "bus-watch-namespace.h" +#include <gio/gio.h> + +enum { + PROP_0, + PROP_HAS_ACTIVE_PLAYER +}; + +struct _MprisController +{ + GObject parent; + + GCancellable *cancellable; + GDBusProxy *mpris_client_proxy; + guint namespace_watcher_id; + GSList *other_proxies; +}; + +G_DEFINE_TYPE (MprisController, mpris_controller, G_TYPE_OBJECT) + +static void +mpris_controller_dispose (GObject *object) +{ + MprisController *self = MPRIS_CONTROLLER (object); + + g_clear_object (&self->cancellable); + g_clear_object (&self->mpris_client_proxy); + + if (self->namespace_watcher_id) + { + bus_unwatch_namespace (self->namespace_watcher_id); + self->namespace_watcher_id = 0; + } + + g_slist_free_full (g_steal_pointer (&self->other_proxies), g_object_unref); + + G_OBJECT_CLASS (mpris_controller_parent_class)->dispose (object); +} + +static void +mpris_proxy_call_done (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + GError *error = NULL; + GVariant *ret; + + if (!(ret = g_dbus_proxy_call_finish (G_DBUS_PROXY (object), res, &error))) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Error calling method %s", error->message); + g_clear_error (&error); + return; + } + g_variant_unref (ret); +} + +gboolean +mpris_controller_key (MprisController *self, const gchar *key) +{ + if (!self->mpris_client_proxy) + return FALSE; + + if (g_strcmp0 (key, "Play") == 0) + key = "PlayPause"; + + g_debug ("calling %s over dbus to mpris client %s", + key, g_dbus_proxy_get_name (self->mpris_client_proxy)); + g_dbus_proxy_call (self->mpris_client_proxy, + key, NULL, 0, -1, self->cancellable, + mpris_proxy_call_done, + NULL); + return TRUE; +} + +static gboolean +mpris_client_is_playing (GDBusProxy *proxy) +{ + g_autoptr(GVariant) playback_status; + const gchar *status_str; + + playback_status = g_dbus_proxy_get_cached_property (proxy, "PlaybackStatus"); + if (!playback_status) + return FALSE; + + if (!g_variant_is_of_type (playback_status, G_VARIANT_TYPE_STRING)) + return FALSE; + + status_str = g_variant_get_string (playback_status, NULL); + return g_strcmp0 (status_str, "Playing") == 0; +} + +static void +mpris_client_notify_name_owner_cb (GDBusProxy *proxy, + GParamSpec *pspec, + MprisController *self) +{ + g_autofree gchar *name_owner = NULL; + GSList *first; + + /* Owner changed, but the proxy is still valid. */ + name_owner = g_dbus_proxy_get_name_owner (proxy); + if (name_owner) + return; + + if (proxy == self->mpris_client_proxy) + { + g_debug ("Clearing the current MPRIS client proxy"); + g_clear_object (&self->mpris_client_proxy); + + if ((first = self->other_proxies)) + { + self->mpris_client_proxy = first->data; + self->other_proxies = first->next; + g_slist_free_1 (first); + + g_debug ("Falling back to MPRIS client %s", + g_dbus_proxy_get_name (self->mpris_client_proxy)); + } + else + { + g_object_notify (G_OBJECT (self), "has-active-player"); + } + } + else + { + g_debug ("Forgetting MPRIS client %s", g_dbus_proxy_get_name (proxy)); + self->other_proxies = g_slist_remove (self->other_proxies, proxy); + g_object_unref (proxy); + } +} + +static void +mpris_client_properties_changed_cb (GDBusProxy *proxy, + GVariant *changed_properties, + GStrv invalidated_properties, + gpointer user_data) +{ + MprisController *self = MPRIS_CONTROLLER (user_data); + GDBusProxy *current_proxy; + + current_proxy = self->mpris_client_proxy; + if (current_proxy == proxy) + return; + + if (current_proxy && mpris_client_is_playing (current_proxy)) + return; + + if (mpris_client_is_playing (proxy)) + { + g_debug ("Switching to MPRIS client %s because it is playing", + g_dbus_proxy_get_name (proxy)); + + self->other_proxies = g_slist_remove (self->other_proxies, proxy); + + if (current_proxy) + self->other_proxies = g_slist_prepend (self->other_proxies, current_proxy); + + self->mpris_client_proxy = proxy; + + if (!current_proxy) + g_object_notify (user_data, "has-active-player"); + } +} + +static void +mpris_proxy_ready_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + MprisController *self = MPRIS_CONTROLLER (user_data); + GError *error = NULL; + GDBusProxy *proxy; + const gchar *name; + + proxy = g_dbus_proxy_new_for_bus_finish (res, &error); + + if (!proxy) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Error connecting to MPRIS interface: %s", error->message); + g_clear_error (&error); + return; + } + + g_signal_connect (proxy, "notify::g-name-owner", + G_CALLBACK (mpris_client_notify_name_owner_cb), user_data); + + g_signal_connect (proxy, "g-properties-changed", + G_CALLBACK (mpris_client_properties_changed_cb), user_data); + + name = g_dbus_proxy_get_name (proxy); + + if (self->mpris_client_proxy) + { + if (mpris_client_is_playing (self->mpris_client_proxy)) + { + g_debug ("Remembering %s for later because the current MPRIS client is playing", + name); + self->other_proxies = g_slist_prepend (self->other_proxies, proxy); + return; + } + + g_debug ("Remembering the current MPRIS client for later"); + self->other_proxies = + g_slist_prepend (self->other_proxies, self->mpris_client_proxy); + } + + g_debug ("Switching to MPRIS client %s because it just appeared", name); + + self->mpris_client_proxy = proxy; + + g_object_notify (user_data, "has-active-player"); +} + +static void +start_mpris_proxy (MprisController *self, const gchar *name) +{ + g_debug ("Creating proxy for %s", name); + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SESSION, + 0, + NULL, + name, + "/org/mpris/MediaPlayer2", + "org.mpris.MediaPlayer2.Player", + self->cancellable, + mpris_proxy_ready_cb, + self); +} + +static void +mpris_player_appeared (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer user_data) +{ + start_mpris_proxy (MPRIS_CONTROLLER (user_data), name); +} + +static void +mpris_controller_constructed (GObject *object) +{ + MprisController *self = MPRIS_CONTROLLER (object); + + self->namespace_watcher_id = bus_watch_namespace (G_BUS_TYPE_SESSION, + "org.mpris.MediaPlayer2", + mpris_player_appeared, + NULL, + MPRIS_CONTROLLER (object), + NULL); +} + +static void +mpris_controller_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MprisController *self = MPRIS_CONTROLLER (object); + + switch (prop_id) { + case PROP_HAS_ACTIVE_PLAYER: + g_value_set_boolean (value, + mpris_controller_get_has_active_player (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +mpris_controller_class_init (MprisControllerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = mpris_controller_constructed; + object_class->dispose = mpris_controller_dispose; + object_class->get_property = mpris_controller_get_property; + + g_object_class_install_property (object_class, + PROP_HAS_ACTIVE_PLAYER, + g_param_spec_boolean ("has-active-player", + NULL, + NULL, + FALSE, + G_PARAM_READABLE)); +} + +static void +mpris_controller_init (MprisController *self) +{ +} + +gboolean +mpris_controller_get_has_active_player (MprisController *controller) +{ + g_return_val_if_fail (MPRIS_IS_CONTROLLER (controller), FALSE); + + return (controller->mpris_client_proxy != NULL); +} + +MprisController * +mpris_controller_new (void) +{ + return g_object_new (MPRIS_TYPE_CONTROLLER, NULL); +} diff --git a/plugins/media-keys/mpris-controller.h b/plugins/media-keys/mpris-controller.h new file mode 100644 index 0000000..a0044c4 --- /dev/null +++ b/plugins/media-keys/mpris-controller.h @@ -0,0 +1,36 @@ +/* + * Copyright © 2013 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, see <http://www.gnu.org/licenses> + * + * Author: Michael Wood <michael.g.wood@intel.com> + */ + +#ifndef __MPRIS_CONTROLLER_H__ +#define __MPRIS_CONTROLLER_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define MPRIS_TYPE_CONTROLLER mpris_controller_get_type() + +G_DECLARE_FINAL_TYPE (MprisController, mpris_controller, MPRIS, CONTROLLER, GObject) + +MprisController *mpris_controller_new (void); +gboolean mpris_controller_key (MprisController *self, const gchar *key); +gboolean mpris_controller_get_has_active_player (MprisController *controller); + +G_END_DECLS + +#endif /* __MPRIS_CONTROLLER_H__ */ diff --git a/plugins/media-keys/org.gnome.ShellKeyGrabber.xml b/plugins/media-keys/org.gnome.ShellKeyGrabber.xml new file mode 100644 index 0000000..df7b8d5 --- /dev/null +++ b/plugins/media-keys/org.gnome.ShellKeyGrabber.xml @@ -0,0 +1,27 @@ +<node> + <interface name="org.gnome.Shell"> + <annotation name="org.gtk.GDBus.C.Name" value="KeyGrabber"/> + <method name="GrabAccelerator"> + <arg type="s" direction="in" name="accelerator"/> + <arg type="u" direction="in" name="modeFlags"/> + <arg type="u" direction="in" name="grabFlags"/> + <arg type="u" direction="out" name="action"/> + </method> + <method name="GrabAccelerators"> + <arg type="a(suu)" direction="in" name="accelerator"/> + <arg type="au" direction="out" name="action"/> + </method> + <method name="UngrabAccelerator"> + <arg type="u" direction="in" name="action"/> + <arg type="b" direction="out" name="success"/> + </method> + <method name="UngrabAccelerators"> + <arg type="au" direction="in" name="action"/> + <arg type="b" direction="out" name="success"/> + </method> + <signal name="AcceleratorActivated"> + <arg type="u" name="action"/> + <arg type="a{sv}" name="parameters"/> + </signal> + </interface> +</node> diff --git a/plugins/media-keys/shell-action-modes.h b/plugins/media-keys/shell-action-modes.h new file mode 100644 index 0000000..f17a7a0 --- /dev/null +++ b/plugins/media-keys/shell-action-modes.h @@ -0,0 +1,53 @@ +/** + * ShellActionMode: + * @SHELL_ACTION_MODE_NONE: block action + * @SHELL_ACTION_MODE_NORMAL: allow action when in window mode, + * e.g. when the focus is in an application window + * @SHELL_ACTION_MODE_OVERVIEW: allow action while the overview + * is active + * @SHELL_ACTION_MODE_LOCK_SCREEN: allow action when the screen + * is locked, e.g. when the screen shield is shown + * @SHELL_ACTION_MODE_UNLOCK_SCREEN: allow action in the unlock + * dialog + * @SHELL_ACTION_MODE_LOGIN_SCREEN: allow action in the login screen + * @SHELL_ACTION_MODE_SYSTEM_MODAL: allow action when a system modal + * dialog (e.g. authentification or session dialogs) is open + * @SHELL_ACTION_MODE_LOOKING_GLASS: allow action in looking glass + * @SHELL_ACTION_MODE_POPUP: allow action while a shell menu is open + * @SHELL_ACTION_MODE_ALL: always allow action + * + * Controls in which GNOME Shell states an action (like keybindings and gestures) + * should be handled. +*/ +typedef enum { + SHELL_ACTION_MODE_NONE = 0, + SHELL_ACTION_MODE_NORMAL = 1 << 0, + SHELL_ACTION_MODE_OVERVIEW = 1 << 1, + SHELL_ACTION_MODE_LOCK_SCREEN = 1 << 2, + SHELL_ACTION_MODE_UNLOCK_SCREEN = 1 << 3, + SHELL_ACTION_MODE_LOGIN_SCREEN = 1 << 4, + SHELL_ACTION_MODE_SYSTEM_MODAL = 1 << 5, + SHELL_ACTION_MODE_LOOKING_GLASS = 1 << 6, + SHELL_ACTION_MODE_POPUP = 1 << 7, + + SHELL_ACTION_MODE_ALL = ~0, +} ShellActionMode; + +/** + * MetaKeyBindingFlags: + * @META_KEY_BINDING_NONE: none + * @META_KEY_BINDING_PER_WINDOW: per-window + * @META_KEY_BINDING_BUILTIN: built-in + * @META_KEY_BINDING_IS_REVERSED: is reversed + * @META_KEY_BINDING_NON_MASKABLE: always active + * @META_KEY_BINDING_IGNORE_AUTOREPEAT: ignore key autorepeat + */ +typedef enum +{ + META_KEY_BINDING_NONE, + META_KEY_BINDING_PER_WINDOW = 1 << 0, + META_KEY_BINDING_BUILTIN = 1 << 1, + META_KEY_BINDING_IS_REVERSED = 1 << 2, + META_KEY_BINDING_NON_MASKABLE = 1 << 3, + META_KEY_BINDING_IGNORE_AUTOREPEAT = 1 << 4, +} MetaKeyBindingFlags;
\ No newline at end of file diff --git a/plugins/media-keys/shortcuts-list.h b/plugins/media-keys/shortcuts-list.h new file mode 100644 index 0000000..8ced72d --- /dev/null +++ b/plugins/media-keys/shortcuts-list.h @@ -0,0 +1,110 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2001 Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __SHORTCUTS_LIST_H__ +#define __SHORTCUTS_LIST_H__ + +#include "shell-action-modes.h" +#include "media-keys.h" + +#define SETTINGS_BINDING_DIR "org.gnome.settings-daemon.plugins.media-keys" + +#define GSD_ACTION_MODE_LAUNCHER (SHELL_ACTION_MODE_NORMAL | \ + SHELL_ACTION_MODE_OVERVIEW) +#define SCREENSAVER_MODE SHELL_ACTION_MODE_ALL & ~SHELL_ACTION_MODE_UNLOCK_SCREEN +#define NO_LOCK_MODE SCREENSAVER_MODE & ~SHELL_ACTION_MODE_LOCK_SCREEN +#define POWER_KEYS_MODE_NO_DIALOG (SHELL_ACTION_MODE_LOCK_SCREEN | \ + SHELL_ACTION_MODE_UNLOCK_SCREEN) +#define POWER_KEYS_MODE (SHELL_ACTION_MODE_NORMAL | \ + SHELL_ACTION_MODE_OVERVIEW | \ + SHELL_ACTION_MODE_LOGIN_SCREEN |\ + POWER_KEYS_MODE_NO_DIALOG) + +static struct { + MediaKeyType key_type; + const char *settings_key; + gboolean static_setting; + ShellActionMode modes; + MetaKeyBindingFlags grab_flags; +} media_keys[] = { + { TOUCHPAD_KEY, "touchpad-toggle", TRUE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { TOUCHPAD_ON_KEY, "touchpad-on", TRUE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { TOUCHPAD_OFF_KEY, "touchpad-off", TRUE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { MUTE_KEY, "volume-mute", TRUE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { VOLUME_DOWN_KEY, "volume-down", TRUE, SHELL_ACTION_MODE_ALL }, + { VOLUME_UP_KEY, "volume-up", TRUE, SHELL_ACTION_MODE_ALL }, + { MIC_MUTE_KEY, "mic-mute", TRUE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { MUTE_QUIET_KEY, "volume-mute-quiet", TRUE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { VOLUME_DOWN_QUIET_KEY, "volume-down-quiet", TRUE, SHELL_ACTION_MODE_ALL }, + { VOLUME_UP_QUIET_KEY, "volume-up-quiet", TRUE, SHELL_ACTION_MODE_ALL }, + { VOLUME_DOWN_PRECISE_KEY, "volume-down-precise", TRUE, SHELL_ACTION_MODE_ALL }, + { VOLUME_UP_PRECISE_KEY, "volume-up-precise", TRUE, SHELL_ACTION_MODE_ALL }, + { LOGOUT_KEY, "logout", FALSE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { EJECT_KEY, "eject", TRUE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { HOME_KEY, "home", TRUE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { MEDIA_KEY, "media", TRUE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { CALCULATOR_KEY, "calculator", TRUE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { SEARCH_KEY, "search", TRUE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { EMAIL_KEY, "email", TRUE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { CONTROL_CENTER_KEY, "control-center", TRUE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { SCREENSAVER_KEY, "screensaver", TRUE, SCREENSAVER_MODE, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { HELP_KEY, "help", FALSE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { SCREENSHOT_KEY, "screenshot", FALSE, NO_LOCK_MODE, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { WINDOW_SCREENSHOT_KEY, "window-screenshot", FALSE, NO_LOCK_MODE, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { AREA_SCREENSHOT_KEY, "area-screenshot", FALSE, NO_LOCK_MODE, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { SCREENSHOT_CLIP_KEY, "screenshot-clip", FALSE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { WINDOW_SCREENSHOT_CLIP_KEY, "window-screenshot-clip", FALSE, SHELL_ACTION_MODE_NORMAL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { AREA_SCREENSHOT_CLIP_KEY, "area-screenshot-clip", FALSE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { SCREENCAST_KEY, "screencast", FALSE, NO_LOCK_MODE, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { WWW_KEY, "www", TRUE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { PLAY_KEY, "play", TRUE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { PAUSE_KEY, "pause", TRUE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { STOP_KEY, "stop", TRUE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { PREVIOUS_KEY, "previous", TRUE, SHELL_ACTION_MODE_ALL }, + { NEXT_KEY, "next", TRUE, SHELL_ACTION_MODE_ALL }, + { REWIND_KEY, "playback-rewind", TRUE, SHELL_ACTION_MODE_ALL }, + { FORWARD_KEY, "playback-forward", TRUE, SHELL_ACTION_MODE_ALL }, + { REPEAT_KEY, "playback-repeat", TRUE, SHELL_ACTION_MODE_ALL }, + { RANDOM_KEY, "playback-random", TRUE, SHELL_ACTION_MODE_ALL }, + { ROTATE_VIDEO_LOCK_KEY, "rotate-video-lock", TRUE, SHELL_ACTION_MODE_ALL }, + { MAGNIFIER_KEY, "magnifier", FALSE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { SCREENREADER_KEY, "screenreader", FALSE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { ON_SCREEN_KEYBOARD_KEY, "on-screen-keyboard", FALSE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { INCREASE_TEXT_KEY, "increase-text-size", FALSE, SHELL_ACTION_MODE_ALL }, + { DECREASE_TEXT_KEY, "decrease-text-size", FALSE, SHELL_ACTION_MODE_ALL }, + { TOGGLE_CONTRAST_KEY, "toggle-contrast", FALSE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { MAGNIFIER_ZOOM_IN_KEY, "magnifier-zoom-in", FALSE, SHELL_ACTION_MODE_ALL }, + { MAGNIFIER_ZOOM_OUT_KEY, "magnifier-zoom-out", FALSE, SHELL_ACTION_MODE_ALL }, + { POWER_KEY, "power", TRUE, POWER_KEYS_MODE, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + /* the kernel / Xorg names really are like this... */ + { SUSPEND_KEY, "suspend", TRUE, POWER_KEYS_MODE, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { HIBERNATE_KEY, "hibernate", TRUE, POWER_KEYS_MODE, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { SCREEN_BRIGHTNESS_UP_KEY, "screen-brightness-up", TRUE, SHELL_ACTION_MODE_ALL }, + { SCREEN_BRIGHTNESS_DOWN_KEY, "screen-brightness-down", TRUE, SHELL_ACTION_MODE_ALL }, + { SCREEN_BRIGHTNESS_CYCLE_KEY, "screen-brightness-cycle", TRUE, SHELL_ACTION_MODE_ALL }, + { KEYBOARD_BRIGHTNESS_UP_KEY, "keyboard-brightness-up", TRUE, SHELL_ACTION_MODE_ALL }, + { KEYBOARD_BRIGHTNESS_DOWN_KEY, "keyboard-brightness-down", TRUE, SHELL_ACTION_MODE_ALL }, + { KEYBOARD_BRIGHTNESS_TOGGLE_KEY, "keyboard-brightness-toggle", TRUE, SHELL_ACTION_MODE_ALL, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { BATTERY_KEY, "battery-status", TRUE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { RFKILL_KEY, "rfkill", TRUE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT }, + { BLUETOOTH_RFKILL_KEY, "rfkill-bluetooth", TRUE, GSD_ACTION_MODE_LAUNCHER, META_KEY_BINDING_IGNORE_AUTOREPEAT} +}; + +#undef SCREENSAVER_MODE + +#endif /* __SHORTCUTS_LIST_H__ */ diff --git a/plugins/meson-add-wants.sh b/plugins/meson-add-wants.sh new file mode 100755 index 0000000..c33d1b4 --- /dev/null +++ b/plugins/meson-add-wants.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -eu + +# Script copied from systemd + +unitdir="$1" +target="$2" +unit="$3" + +case "$target" in + */?*) # a path, but not just a slash at the end + dir="${DESTDIR:-}${target}" + ;; + *) + dir="${DESTDIR:-}${unitdir}/${target}" + ;; +esac + +unitpath="${DESTDIR:-}${unitdir}/${unit}" + +case "$target" in + */) + mkdir -vp -m 0755 "$dir" + ;; + *) + mkdir -vp -m 0755 "$(dirname "$dir")" + ;; +esac + +ln -vfs --relative "$unitpath" "$dir" diff --git a/plugins/meson.build b/plugins/meson.build new file mode 100644 index 0000000..83e0188 --- /dev/null +++ b/plugins/meson.build @@ -0,0 +1,194 @@ +all_plugins = [ + ['a11y-settings', 'A11ySettings', 'GNOME accessibility'], + ['color', 'Color', 'GNOME color management'], + ['datetime', 'Datetime', 'GNOME date & time'], + ['power', 'Power', 'GNOME power management'], + ['housekeeping', 'Housekeeping', 'GNOME maintenance of expirable data'], + ['keyboard', 'Keyboard', 'GNOME keyboard configuration'], + ['media-keys', 'MediaKeys', 'GNOME keyboard shortcuts'], + ['screensaver-proxy', 'ScreensaverProxy', 'GNOME FreeDesktop screensaver'], + ['sharing', 'Sharing', 'GNOME file sharing'], + ['sound', 'Sound', 'GNOME sound sample caching'], + ['usb-protection', 'UsbProtection', 'GNOME USB protection'], + ['xsettings', 'XSettings', 'GNOME XSettings'], + ['smartcard', 'Smartcard', 'GNOME smartcard'], + ['wacom', 'Wacom', 'GNOME Wacom tablet support'], + ['print-notifications', 'PrintNotifications', 'GNOME printer notifications'], + ['rfkill', 'Rfkill', 'GNOME RFKill support'], + ['wwan', 'Wwan', 'GNOME WWan support'], +] + +disabled_plugins = [] + +if not enable_smartcard + disabled_plugins += ['smartcard'] +endif + +if not enable_usb_protection + disabled_plugins += ['usb-protection'] +endif + +if not enable_wacom + disabled_plugins += ['wacom'] +endif + +if not enable_cups + disabled_plugins += ['cups'] +endif + +if not enable_rfkill + disabled_plugins += ['rfkill'] +endif + +if not enable_wwan + disabled_plugins += ['wwan'] +endif + +if not enable_colord + disabled_plugins += ['color'] +endif + +if not enable_cups + disabled_plugins += ['print-notifications'] +endif + +# Specify futher required units, 'before' or 'after' may be specified if ordering is needed +plugin_gate_units = { + 'xsettings': [ + # Both after/before. after for stopping reliably, before for synchronisation + ['gnome-session-x11-services.target', 'after'], + ['gnome-session-x11-services-ready.target', 'before'], + ], +# 'wacom': [['wacom.target']], +# 'smartcard': [['smartcard.target']], +} + +# Restart=on-failure is the default +plugin_restart_rule = { + 'xsettings' : 'on-abnormal', +} + +plugins_conf = configuration_data() +plugins_conf.set('libexecdir', gsd_libexecdir) + +plugins_deps = [libgsd_dep] + +plugins_cflags = ['-DGNOME_SETTINGS_LOCALEDIR="@0@"'.format(gsd_localedir)] + +all_plugins_file = [] + +cflags = [ + '-DG_LOG_DOMAIN="common"' +] + plugins_cflags +plugin_name = 'common' +subdir('common') + +foreach plugin: all_plugins + plugin_name = plugin[0] + plugin_name_case = plugin[1] + plugin_description = plugin[2] + plugin_dbus_name='org.gnome.SettingsDaemon.@0@'.format(plugin_name_case) + + desktop = 'org.gnome.SettingsDaemon.@0@.desktop'.format(plugin[1]) + + if disabled_plugins.contains(plugin_name) + desktop_in_file = files('org.gnome.SettingsDaemon.Dummy.desktop.in') + else + desktop_in_file = files('org.gnome.SettingsDaemon.Real.desktop.in') + endif + + cflags = [ + '-DG_LOG_DOMAIN="@0@-plugin"'.format(plugin_name), + '-DPLUGIN_NAME="@0@"'.format(plugin_name), + '-DPLUGIN_DBUS_NAME="@0@"'.format(plugin_dbus_name), + ] + plugins_cflags + + desktop = 'org.gnome.SettingsDaemon.@0@.desktop'.format(plugin[1]) + desktop_conf = configuration_data() + desktop_conf.set('libexecdir', gsd_libexecdir) + desktop_conf.set('systemd_hidden', enable_systemd ? 'true' : 'false') + desktop_conf.set('pluginname', plugin_name) + desktop_conf.set('description', plugin_description) + configure_file( + input: desktop_in_file, + output: desktop, + configuration: desktop_conf, + install: true, + install_dir: gsd_xdg_autostart + ) + + if not disabled_plugins.contains(plugin_name) + user_target = 'org.gnome.SettingsDaemon.@0@.target'.format(plugin[1]) + user_service = 'org.gnome.SettingsDaemon.@0@.service'.format(plugin[1]) + + unit_conf = configuration_data() + unit_conf.set('plugin_name', plugin_name) + unit_conf.set('description', plugin_description) + unit_conf.set('libexecdir', gsd_libexecdir) + unit_conf.set('plugin_dbus_name', plugin_dbus_name) + unit_conf.set('plugin_restart', plugin_restart_rule.get(plugin_name, 'on-failure')) + + gates_all = [] + gates_after = [] + gates_before = [] + foreach gate: plugin_gate_units.get(plugin_name, []) + gates_all += [gate[0]] + if gate.length() > 1 + if gate[1] == 'before' + gates_before += [gate[0]] + elif gate[1] == 'after' + gates_after += [gate[0]] + else + error('Ordering key must be either "before" or "after"') + endif + endif + endforeach + gate_unit_section = [] + if gates_all.length() > 0 + gate_unit_section += ['Requisite=' + ' '.join(gates_all)] + gate_unit_section += ['PartOf=' + ' '.join(gates_all)] + + if gates_after.length() > 0 + gate_unit_section += ['After=' + ' '.join(gates_after)] + endif + if gates_before.length() > 0 + gate_unit_section += ['Before=' + ' '.join(gates_before)] + endif + endif + unit_conf.set('plugin_gate_units_section', '\n'.join(gate_unit_section)) + + if enable_systemd + configure_file( + input: 'gsd.service.in', + output: user_service, + configuration: unit_conf, + install: true, + install_dir: systemd_userunitdir + ) + configure_file( + input: 'gsd.target.in', + output: user_target, + configuration: unit_conf, + install: true, + install_dir: systemd_userunitdir + ) + + # Wipe out old target names if our prefix differes from the + # systemd one, i.e. we are probably in a virtual environment and + # may be picking up old units from a system installation. + # This saves a lot of pain when running a new g-s-d inside + # jhbuild on an old host. + # TODO: Should be deleted once we can assume developers have 3.38 + # installed on their machines. + if gsd_prefix != systemd_dep.get_pkgconfig_variable('prefix') + meson.add_install_script('sh', '-c', 'ln -vfs /dev/null "${DESTDIR:-}$1"', 'sh', systemd_userunitdir / 'gsd-@0@.target'.format(plugin_name)) + endif + + foreach target: gates_all + meson.add_install_script('meson-add-wants.sh', systemd_userunitdir, target + '.wants/', user_service) + endforeach + endif + + subdir(plugin_name) + endif +endforeach diff --git a/plugins/org.gnome.SettingsDaemon.Dummy.desktop.in b/plugins/org.gnome.SettingsDaemon.Dummy.desktop.in new file mode 100644 index 0000000..04249c8 --- /dev/null +++ b/plugins/org.gnome.SettingsDaemon.Dummy.desktop.in @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name=GNOME Settings Daemon's @pluginname@ dummy autostart file +Exec=/bin/false +OnlyShowIn=GNOME; +NoDisplay=true +Hidden=true diff --git a/plugins/org.gnome.SettingsDaemon.Real.desktop.in b/plugins/org.gnome.SettingsDaemon.Real.desktop.in new file mode 100644 index 0000000..3034e71 --- /dev/null +++ b/plugins/org.gnome.SettingsDaemon.Real.desktop.in @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=@description@ +Exec=@libexecdir@/gsd-@pluginname@ +OnlyShowIn=GNOME; +NoDisplay=true +X-GNOME-Autostart-Phase=Initialization +X-GNOME-Autostart-Notify=true +X-GNOME-AutoRestart=true +X-GNOME-HiddenUnderSystemd=@systemd_hidden@ diff --git a/plugins/power/gpm-common.c b/plugins/power/gpm-common.c new file mode 100644 index 0000000..a7ca87f --- /dev/null +++ b/plugins/power/gpm-common.c @@ -0,0 +1,456 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2005-2011 Richard Hughes <richard@hughsie.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <sys/wait.h> +#include <math.h> +#include <glib.h> +#include <glib/gi18n.h> +#include <gdk/gdkx.h> +#include <X11/extensions/dpms.h> +#include <canberra-gtk.h> + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include <libgnome-desktop/gnome-rr.h> + +#include "gnome-settings-bus.h" +#include "gpm-common.h" +#include "gsd-power-constants.h" +#include "gsd-power-manager.h" + +#define XSCREENSAVER_WATCHDOG_TIMEOUT 120 /* seconds */ +#define UPS_SOUND_LOOP_ID 99 +#define GSD_POWER_MANAGER_CRITICAL_ALERT_TIMEOUT 5 /* seconds */ + +static int +gsd_power_backlight_convert_safe (int value, int from_range, int to_range) +{ + /* round (value / from_range) * to_range */ + return (value * to_range + from_range / 2) / from_range; +} + +/* take a discrete value with offset and convert to percentage */ +int +gsd_power_backlight_abs_to_percentage (int min, int max, int value) +{ + g_return_val_if_fail (max > min, -1); + g_return_val_if_fail (value >= min, -1); + g_return_val_if_fail (value <= max, -1); + return gsd_power_backlight_convert_safe (value - min, max - min, 100); +} + +/* take a percentage and convert to a discrete value with offset */ +int +gsd_power_backlight_percentage_to_abs (int min, int max, int value) +{ + g_return_val_if_fail (max > min, -1); + g_return_val_if_fail (value >= 0, -1); + g_return_val_if_fail (value <= 100, -1); + + return min + gsd_power_backlight_convert_safe (value, 100, max - min); +} + +#define GPM_UP_TIME_PRECISION 5*60 +#define GPM_UP_TEXT_MIN_TIME 120 + +/** + * Return value: The time string, e.g. "2 hours 3 minutes" + **/ +gchar * +gpm_get_timestring (guint time_secs) +{ + char* timestring = NULL; + gint hours; + gint minutes; + + /* Add 0.5 to do rounding */ + minutes = (int) ( ( time_secs / 60.0 ) + 0.5 ); + + if (minutes == 0) { + timestring = g_strdup (_("Unknown time")); + return timestring; + } + + if (minutes < 60) { + timestring = g_strdup_printf (ngettext ("%i minute", + "%i minutes", + minutes), minutes); + return timestring; + } + + hours = minutes / 60; + minutes = minutes % 60; + if (minutes == 0) + timestring = g_strdup_printf (ngettext ( + "%i hour", + "%i hours", + hours), hours); + else + /* TRANSLATOR: "%i %s %i %s" are "%i hours %i minutes" + * Swap order with "%2$s %2$i %1$s %1$i if needed */ + timestring = g_strdup_printf (_("%i %s %i %s"), + hours, ngettext ("hour", "hours", hours), + minutes, ngettext ("minute", "minutes", minutes)); + return timestring; +} + +static gboolean +parse_vm_kernel_cmdline (gboolean *is_virtual_machine) +{ + gboolean ret = FALSE; + GRegex *regex; + GMatchInfo *match; + char *contents; + char *word; + const char *arg; + + if (!g_file_get_contents ("/proc/cmdline", &contents, NULL, NULL)) + return ret; + + regex = g_regex_new ("gnome.is_vm=(\\S+)", 0, G_REGEX_MATCH_NOTEMPTY, NULL); + if (!g_regex_match (regex, contents, G_REGEX_MATCH_NOTEMPTY, &match)) + goto out; + + word = g_match_info_fetch (match, 0); + g_debug ("Found command-line match '%s'", word); + arg = word + strlen ("gnome.is_vm="); + if (*arg != '0' && *arg != '1') { + g_warning ("Invalid value '%s' for gnome.is_vm passed in kernel command line.\n", arg); + } else { + *is_virtual_machine = atoi (arg); + ret = TRUE; + } + g_free (word); + +out: + g_match_info_free (match); + g_regex_unref (regex); + g_free (contents); + + if (ret) + g_debug ("Kernel command-line parsed to %d", *is_virtual_machine); + + return ret; +} + +gboolean +gsd_power_is_hardware_a_vm (void) +{ + const gchar *str; + gboolean ret = FALSE; + GError *error = NULL; + GVariant *inner; + GVariant *variant = NULL; + GDBusConnection *connection; + + if (parse_vm_kernel_cmdline (&ret)) + return ret; + + connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, + NULL, + &error); + if (connection == NULL) { + g_warning ("system bus not available: %s", error->message); + g_error_free (error); + goto out; + } + variant = g_dbus_connection_call_sync (connection, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.DBus.Properties", + "Get", + g_variant_new ("(ss)", + "org.freedesktop.systemd1.Manager", + "Virtualization"), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &error); + if (variant == NULL) { + g_debug ("Failed to get property '%s': %s", "Virtualization", error->message); + g_error_free (error); + goto out; + } + + /* on bare-metal hardware this is the empty string, + * otherwise an identifier such as "kvm", "vmware", etc. */ + g_variant_get (variant, "(v)", &inner); + str = g_variant_get_string (inner, NULL); + if (str != NULL && str[0] != '\0') + ret = TRUE; + g_variant_unref (inner); +out: + if (connection != NULL) + g_object_unref (connection); + if (variant != NULL) + g_variant_unref (variant); + return ret; +} + +/* This timer goes off every few minutes, whether the user is idle or not, + to try and clean up anything that has gone wrong. + + It calls disable_builtin_screensaver() so that if xset has been used, + or some other program (like xlock) has messed with the XSetScreenSaver() + settings, they will be set back to sensible values (if a server extension + is in use, messing with xlock can cause the screensaver to never get a wakeup + event, and could cause monitor power-saving to occur, and all manner of + heinousness.) + + This code was originally part of gnome-screensaver, see + http://git.gnome.org/browse/gnome-screensaver/tree/src/gs-watcher-x11.c?id=fec00b12ec46c86334cfd36b37771cc4632f0d4d#n530 + */ +static gboolean +disable_builtin_screensaver (gpointer unused) +{ + int current_server_timeout, current_server_interval; + int current_prefer_blank, current_allow_exp; + int desired_server_timeout, desired_server_interval; + int desired_prefer_blank, desired_allow_exp; + + XGetScreenSaver (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), + ¤t_server_timeout, + ¤t_server_interval, + ¤t_prefer_blank, + ¤t_allow_exp); + + desired_server_timeout = current_server_timeout; + desired_server_interval = current_server_interval; + desired_prefer_blank = current_prefer_blank; + desired_allow_exp = current_allow_exp; + + desired_server_interval = 0; + + /* I suspect (but am not sure) that DontAllowExposures might have + something to do with powering off the monitor as well, at least + on some systems that don't support XDPMS? Who know... */ + desired_allow_exp = AllowExposures; + + /* When we're not using an extension, set the server-side timeout to 0, + so that the server never gets involved with screen blanking, and we + do it all ourselves. (However, when we *are* using an extension, + we tell the server when to notify us, and rather than blanking the + screen, the server will send us an X event telling us to blank.) + */ + desired_server_timeout = 0; + + if (desired_server_timeout != current_server_timeout + || desired_server_interval != current_server_interval + || desired_prefer_blank != current_prefer_blank + || desired_allow_exp != current_allow_exp) { + + g_debug ("disabling server builtin screensaver:" + " (xset s %d %d; xset s %s; xset s %s)", + desired_server_timeout, + desired_server_interval, + (desired_prefer_blank ? "blank" : "noblank"), + (desired_allow_exp ? "expose" : "noexpose")); + + XSetScreenSaver (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), + desired_server_timeout, + desired_server_interval, + desired_prefer_blank, + desired_allow_exp); + + XSync (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), FALSE); + } + + return TRUE; +} + +guint +gsd_power_enable_screensaver_watchdog (void) +{ + int dummy; + guint id; + + /* Make sure that Xorg's DPMS extension never gets in our + * way. The defaults are now applied in Fedora 20 from + * being "0" by default to being "600" by default */ + gdk_x11_display_error_trap_push (gdk_display_get_default ()); + if (DPMSQueryExtension(GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), &dummy, &dummy)) + DPMSSetTimeouts (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), 0, 0, 0); + gdk_x11_display_error_trap_pop_ignored (gdk_display_get_default ()); + id = g_timeout_add_seconds (XSCREENSAVER_WATCHDOG_TIMEOUT, + disable_builtin_screensaver, + NULL); + g_source_set_name_by_id (id, "[gnome-settings-daemon] disable_builtin_screensaver"); + return id; +} + +static gpointer +parse_mock_mock_external_monitor (gpointer data) +{ + const char *mocked_file; + mocked_file = g_getenv ("GSD_MOCK_EXTERNAL_MONITOR_FILE"); + + return g_strdup (mocked_file); +} + +static const gchar * +get_mock_external_monitor_file (void) +{ + static GOnce mocked_once = G_ONCE_INIT; + g_once (&mocked_once, parse_mock_mock_external_monitor, NULL); + return mocked_once.retval; +} + +static gboolean +randr_output_is_on (GnomeRROutput *output) +{ + GnomeRRCrtc *crtc; + + crtc = gnome_rr_output_get_crtc (output); + if (!crtc) + return FALSE; + return gnome_rr_crtc_get_current_mode (crtc) != NULL; +} + +static void +mock_monitor_changed (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + GnomeRRScreen *screen = (GnomeRRScreen *) user_data; + + g_debug ("Mock screen configuration changed"); + g_signal_emit_by_name (G_OBJECT (screen), "changed"); +} + +static void +screen_destroyed (gpointer user_data, + GObject *where_the_object_was) +{ + g_object_unref (G_OBJECT (user_data)); +} + +void +watch_external_monitor (GnomeRRScreen *screen) +{ + const gchar *filename; + GFile *file; + GFileMonitor *monitor; + + filename = get_mock_external_monitor_file (); + if (!filename) + return; + + file = g_file_new_for_commandline_arg (filename); + monitor = g_file_monitor (file, G_FILE_MONITOR_NONE, NULL, NULL); + g_object_unref (file); + g_signal_connect (monitor, "changed", + G_CALLBACK (mock_monitor_changed), screen); + g_object_weak_ref (G_OBJECT (screen), screen_destroyed, monitor); +} + +static gboolean +mock_external_monitor_is_connected (GnomeRRScreen *screen) +{ + char *mock_external_monitor_contents; + const gchar *filename; + + filename = get_mock_external_monitor_file (); + g_assert (filename); + + if (g_file_get_contents (filename, &mock_external_monitor_contents, NULL, NULL)) { + if (mock_external_monitor_contents[0] == '1') { + g_free (mock_external_monitor_contents); + g_debug ("Mock external monitor is on"); + return TRUE; + } else if (mock_external_monitor_contents[0] == '0') { + g_free (mock_external_monitor_contents); + g_debug ("Mock external monitor is off"); + return FALSE; + } + + g_error ("Unhandled value for GSD_MOCK_EXTERNAL_MONITOR contents: %s", mock_external_monitor_contents); + g_free (mock_external_monitor_contents); + } + + return FALSE; +} + +gboolean +external_monitor_is_connected (GnomeRRScreen *screen) +{ + GnomeRROutput **outputs; + guint i; + + if (get_mock_external_monitor_file ()) + return mock_external_monitor_is_connected (screen); + + g_assert (screen != NULL); + + /* see if we have more than one screen plugged in */ + outputs = gnome_rr_screen_list_outputs (screen); + for (i = 0; outputs[i] != NULL; i++) { + if (randr_output_is_on (outputs[i]) && + !gnome_rr_output_is_builtin_display (outputs[i])) + return TRUE; + } + + return FALSE; +} + +static void +play_sound (void) +{ + ca_context_play (ca_gtk_context_get (), UPS_SOUND_LOOP_ID, + CA_PROP_EVENT_ID, "battery-caution", + CA_PROP_EVENT_DESCRIPTION, _("Battery is critically low"), NULL); +} + +static gboolean +play_loop_timeout_cb (gpointer user_data) +{ + play_sound (); + return TRUE; +} + +void +play_loop_start (guint *id) +{ + if (*id != 0) + return; + + *id = g_timeout_add_seconds (GSD_POWER_MANAGER_CRITICAL_ALERT_TIMEOUT, + (GSourceFunc) play_loop_timeout_cb, + NULL); + g_source_set_name_by_id (*id, "[gnome-settings-daemon] play_loop_timeout_cb"); + play_sound (); +} + +void +play_loop_stop (guint *id) +{ + if (*id == 0) + return; + + ca_context_cancel (ca_gtk_context_get (), UPS_SOUND_LOOP_ID); + g_source_remove (*id); + *id = 0; +} diff --git a/plugins/power/gpm-common.h b/plugins/power/gpm-common.h new file mode 100644 index 0000000..88a8e00 --- /dev/null +++ b/plugins/power/gpm-common.h @@ -0,0 +1,59 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2005-2011 Richard Hughes <richard@hughsie.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __GPMCOMMON_H +#define __GPMCOMMON_H + +#include <glib.h> +#include <libupower-glib/upower.h> + +G_BEGIN_DECLS + +/* UPower helpers */ +gchar *gpm_get_timestring (guint time); + +/* Power helpers */ +gboolean gsd_power_is_hardware_a_vm (void); +guint gsd_power_enable_screensaver_watchdog (void); +void reset_idletime (void); + +/* Backlight helpers */ + +/* on ACPI machines we have 4-16 levels, on others it's ~150 */ +#define BRIGHTNESS_STEP_AMOUNT(max) ((max) < 20 ? 1 : (max) / 20) + +#define ABS_TO_PERCENTAGE(min, max, value) gsd_power_backlight_abs_to_percentage(min, max, value) +#define PERCENTAGE_TO_ABS(min, max, value) gsd_power_backlight_percentage_to_abs(min, max, value) + +int gsd_power_backlight_abs_to_percentage (int min, int max, int value); +int gsd_power_backlight_percentage_to_abs (int min, int max, int value); + +/* RandR helpers */ +void watch_external_monitor (GnomeRRScreen *screen); +gboolean external_monitor_is_connected (GnomeRRScreen *screen); + +/* Sound helpers */ +void play_loop_start (guint *id); +void play_loop_stop (guint *id); + +G_END_DECLS + +#endif /* __GPMCOMMON_H */ diff --git a/plugins/power/gsd-backlight-helper.c b/plugins/power/gsd-backlight-helper.c new file mode 100644 index 0000000..c370c79 --- /dev/null +++ b/plugins/power/gsd-backlight-helper.c @@ -0,0 +1,153 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2018 Benjamin Berg <bberg@redhat.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <stdlib.h> +#include <dirent.h> +#include <errno.h> +#include <string.h> +#include <libgen.h> +#include <stdio.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> + +#define GSD_BACKLIGHT_HELPER_EXIT_CODE_SUCCESS 0 +#define GSD_BACKLIGHT_HELPER_EXIT_CODE_FAILED 1 +#define GSD_BACKLIGHT_HELPER_EXIT_CODE_ARGUMENTS_INVALID 3 +#define GSD_BACKLIGHT_HELPER_EXIT_CODE_INVALID_USER 4 + +#ifndef __linux__ +#error "gsd-backlight-helper does not work on non-Linux" +#endif + +static void +usage(int argc, char *argv[]) +{ + fprintf (stderr, "Usage: %s device brightness\n", argv[0]); + fprintf (stderr, " device: The backlight directory starting with \"/sys/class/backlight/\"\n"); + fprintf (stderr, " brightness: The new brightness to write\n"); +} + +int +main (int argc, char *argv[]) +{ + char tmp[512]; + char *device = NULL; + int fd, len, res; + int uid, euid; + int brightness; + int result = GSD_BACKLIGHT_HELPER_EXIT_CODE_FAILED; + DIR *dp = NULL; + struct dirent *ep; + + /* check calling UID */ + uid = getuid (); + euid = geteuid (); + if (uid != 0 || euid != 0) { + fprintf (stderr, "This program can only be used by the root user\n"); + result = GSD_BACKLIGHT_HELPER_EXIT_CODE_INVALID_USER; + goto done; + } + + if (argc != 3) { + fprintf (stderr, "Error: Need to be called with exactly two arguments\n"); + usage (argc, argv); + result = GSD_BACKLIGHT_HELPER_EXIT_CODE_ARGUMENTS_INVALID; + goto done; + } + + errno = 0; + brightness = strtol (argv[2], NULL, 0); + if (errno) { + fprintf (stderr, "Error: Invalid brightness argument (%d: %s)\n", errno, strerror (errno)); + usage (argc, argv); + goto done; + } + + device = realpath (argv[1], NULL); + if (device == NULL) { + fprintf (stderr, "Error: Could not canonicalize given path (%d: %s)\n", errno, strerror (errno)); + result = GSD_BACKLIGHT_HELPER_EXIT_CODE_FAILED; + goto done; + } + + dp = opendir ("/sys/class/backlight"); + if (dp == NULL) { + fprintf (stderr, "Error: Could not open /sys/class/backlight (%d: %s)\n", errno, strerror (errno)); + result = GSD_BACKLIGHT_HELPER_EXIT_CODE_FAILED; + goto done; + } + + while ((ep = readdir (dp))) { + char *path; + + if (ep->d_name[0] == '.') + continue; + + /* Leave room for "/brightness" */ + snprintf (tmp, sizeof(tmp) - 11, "/sys/class/backlight/%s", ep->d_name); + path = realpath (tmp, NULL); + if (strcmp (path, device) == 0) { + free (path); + strcat (tmp, "/brightness"); + + fd = open (tmp, O_WRONLY); + if (fd < 0) { + fprintf (stderr, "Error: Could not open brightness sysfs file (%d: %s)\n", errno, strerror(errno)); + result = GSD_BACKLIGHT_HELPER_EXIT_CODE_FAILED; + goto done; + } + + len = snprintf (tmp, sizeof(tmp), "%d", brightness); + if ((res = write (fd, tmp, len)) != len) { + if (res == -1) + fprintf (stderr, "Error: Writing to file (%d: %s)\n", errno, strerror(errno)); + else + fprintf (stderr, "Error: Wrote the wrong length (%d of %d bytes)!\n", res, len); + + close (fd); + result = GSD_BACKLIGHT_HELPER_EXIT_CODE_FAILED; + goto done; + } + close (fd); + + result = GSD_BACKLIGHT_HELPER_EXIT_CODE_SUCCESS; + goto done; + } else { + free (path); + } + } + + result = GSD_BACKLIGHT_HELPER_EXIT_CODE_FAILED; + fprintf (stderr, "Error: Could not find the specified backlight \"%s\"\n", argv[1]); + +done: + if (device) + free (device); + if (dp) + closedir (dp); + + return result; +} + diff --git a/plugins/power/gsd-backlight.c b/plugins/power/gsd-backlight.c new file mode 100644 index 0000000..ca5f272 --- /dev/null +++ b/plugins/power/gsd-backlight.c @@ -0,0 +1,978 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2018 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" +#include <stdlib.h> + +#include "gsd-backlight.h" +#include "gpm-common.h" +#include "gsd-power-constants.h" +#include "gsd-power-manager.h" + +#ifdef __linux__ +#include <gudev/gudev.h> +#endif /* __linux__ */ + +struct _GsdBacklight +{ + GObject object; + + gint brightness_min; + gint brightness_max; + gint brightness_val; + gint brightness_target; + gint brightness_step; + +#ifdef __linux__ + GDBusProxy *logind_proxy; + + GUdevClient *udev; + GUdevDevice *udev_device; + + GTask *active_task; + GQueue tasks; + + gint idle_update; +#endif /* __linux__ */ + + GnomeRRScreen *rr_screen; + gboolean builtin_display_disabled; +}; + +enum { + PROP_RR_SCREEN = 1, + PROP_BRIGHTNESS, + PROP_LAST, +}; + +#define SYSTEMD_DBUS_NAME "org.freedesktop.login1" +#define SYSTEMD_DBUS_PATH "/org/freedesktop/login1/session/auto" +#define SYSTEMD_DBUS_INTERFACE "org.freedesktop.login1.Session" + +static GParamSpec *props[PROP_LAST]; + +static void gsd_backlight_initable_iface_init (GInitableIface *iface); +static gboolean gsd_backlight_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error); + + +G_DEFINE_TYPE_EXTENDED (GsdBacklight, gsd_backlight, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, + gsd_backlight_initable_iface_init);) + +#ifdef __linux__ +static GUdevDevice* +gsd_backlight_udev_get_type (GList *devices, const gchar *type) +{ + const gchar *type_tmp; + GList *d; + + for (d = devices; d != NULL; d = d->next) { + type_tmp = g_udev_device_get_sysfs_attr (d->data, "type"); + if (g_strcmp0 (type_tmp, type) == 0) + return G_UDEV_DEVICE (g_object_ref (d->data)); + } + return NULL; +} + +/* + * Search for a raw backlight interface, raw backlight interfaces registered + * by the drm driver will have the drm-connector as their parent, check the + * drm-connector's enabled sysfs attribute so that we pick the right LCD-panel + * connector on laptops with hybrid-gfx. Fall back to just picking the first + * raw backlight interface if no enabled interface is found. + */ +static GUdevDevice* +gsd_backlight_udev_get_raw (GList *devices) +{ + GUdevDevice *parent; + const gchar *attr; + GList *d; + + for (d = devices; d != NULL; d = d->next) { + attr = g_udev_device_get_sysfs_attr (d->data, "type"); + if (g_strcmp0 (attr, "raw") != 0) + continue; + + parent = g_udev_device_get_parent (d->data); + if (!parent) + continue; + + attr = g_udev_device_get_sysfs_attr (parent, "enabled"); + if (!attr || g_strcmp0 (attr, "enabled") != 0) + continue; + + return G_UDEV_DEVICE (g_object_ref (d->data)); + } + + return gsd_backlight_udev_get_type (devices, "raw"); +} + +static void +gsd_backlight_udev_resolve (GsdBacklight *backlight) +{ + g_autolist(GUdevDevice) devices = NULL; + + g_assert (backlight->udev != NULL); + + devices = g_udev_client_query_by_subsystem (backlight->udev, "backlight"); + if (devices == NULL) + return; + + /* Search the backlight devices and prefer the types: + * firmware -> platform -> raw */ + backlight->udev_device = gsd_backlight_udev_get_type (devices, "firmware"); + if (backlight->udev_device != NULL) + return; + + backlight->udev_device = gsd_backlight_udev_get_type (devices, "platform"); + if (backlight->udev_device != NULL) + return; + + backlight->udev_device = gsd_backlight_udev_get_raw (devices); + if (backlight->udev_device != NULL) + return; +} + +static gboolean +gsd_backlight_udev_idle_update_cb (GsdBacklight *backlight) +{ + g_autoptr(GError) error = NULL; + gint brightness; + g_autofree gchar *path = NULL; + g_autofree gchar *contents = NULL; + backlight->idle_update = 0; + + /* If we are active again now, just stop. */ + if (backlight->active_task) + return FALSE; + + path = g_build_filename (g_udev_device_get_sysfs_path (backlight->udev_device), "brightness", NULL); + if (!g_file_get_contents (path, &contents, NULL, &error)) { + g_warning ("Could not get brightness from sysfs: %s", error->message); + return FALSE; + } + brightness = g_ascii_strtoll (contents, NULL, 0); + + /* e.g. brightness lower than our minimum. */ + brightness = CLAMP (brightness, backlight->brightness_min, backlight->brightness_max); + + /* Only notify if brightness has changed. */ + if (brightness == backlight->brightness_val) + return FALSE; + + backlight->brightness_val = brightness; + backlight->brightness_target = brightness; + g_object_notify_by_pspec (G_OBJECT (backlight), props[PROP_BRIGHTNESS]); + + return FALSE; +} + +static void +gsd_backlight_udev_idle_update (GsdBacklight *backlight) +{ + if (backlight->idle_update) + return; + + backlight->idle_update = g_idle_add ((GSourceFunc) gsd_backlight_udev_idle_update_cb, backlight); +} + + +static void +gsd_backlight_udev_uevent (GUdevClient *client, const gchar *action, GUdevDevice *device, gpointer user_data) +{ + GsdBacklight *backlight = GSD_BACKLIGHT (user_data); + + if (g_strcmp0 (action, "change") != 0) + return; + + /* We are going to update our state after processing the tasks anyway. */ + if (!g_queue_is_empty (&backlight->tasks)) + return; + + if (g_strcmp0 (g_udev_device_get_sysfs_path (device), + g_udev_device_get_sysfs_path (backlight->udev_device)) != 0) + return; + + g_debug ("GsdBacklight: Got uevent"); + + gsd_backlight_udev_idle_update (backlight); +} + + +static gboolean +gsd_backlight_udev_init (GsdBacklight *backlight) +{ + const gchar* const subsystems[] = {"backlight", NULL}; + gint brightness_val; + + backlight->udev = g_udev_client_new (subsystems); + gsd_backlight_udev_resolve (backlight); + if (backlight->udev_device == NULL) + return FALSE; + + backlight->brightness_max = g_udev_device_get_sysfs_attr_as_int (backlight->udev_device, + "max_brightness"); + backlight->brightness_min = MAX (1, backlight->brightness_max * 0.01); + + /* If the interface has less than 100 possible values, and it is of type + * raw, then assume that 0 does not turn off the backlight completely. */ + if (backlight->brightness_max < 99 && + g_strcmp0 (g_udev_device_get_sysfs_attr (backlight->udev_device, "type"), "raw") == 0) + backlight->brightness_min = 0; + + /* Ignore a backlight which has no steps. */ + if (backlight->brightness_min >= backlight->brightness_max) { + g_warning ("Resolved kernel backlight has an unusable maximum brightness (%d)", backlight->brightness_max); + g_clear_object (&backlight->udev_device); + return FALSE; + } + + brightness_val = g_udev_device_get_sysfs_attr_as_int (backlight->udev_device, + "brightness"); + backlight->brightness_val = CLAMP (brightness_val, + backlight->brightness_min, + backlight->brightness_max); + g_debug ("Using udev device with brightness from %i to %i. Current brightness is %i.", + backlight->brightness_min, backlight->brightness_max, backlight->brightness_val); + + g_signal_connect_object (backlight->udev, "uevent", + G_CALLBACK (gsd_backlight_udev_uevent), + backlight, 0); + + return TRUE; +} + + +typedef struct { + int value; + char *value_str; +} BacklightHelperData; + +static void gsd_backlight_process_taskqueue (GsdBacklight *backlight); + +static void +backlight_task_data_destroy (gpointer data) +{ + BacklightHelperData *task_data = (BacklightHelperData*) data; + + g_free (task_data->value_str); + g_free (task_data); +} + +static void +gsd_backlight_set_helper_return (GsdBacklight *backlight, GTask *task, gint result, const GError *error) +{ + GTask *finished_task; + gint percent = ABS_TO_PERCENTAGE (backlight->brightness_min, backlight->brightness_max, result); + + if (error) + g_warning ("Error executing backlight helper: %s", error->message); + + /* If the queue will be empty then update the current value. */ + if (task == g_queue_peek_tail (&backlight->tasks)) { + if (error == NULL) { + g_assert (backlight->brightness_target == result); + + backlight->brightness_val = backlight->brightness_target; + g_debug ("New brightness value is in effect %i (%i..%i)", + backlight->brightness_val, backlight->brightness_min, backlight->brightness_max); + g_object_notify_by_pspec (G_OBJECT (backlight), props[PROP_BRIGHTNESS]); + } + + /* The udev handler won't read while a write is pending, so queue an + * update in case we have missed some events. */ + gsd_backlight_udev_idle_update (backlight); + } + + /* Return all the pending tasks up and including the one we actually + * processed. */ + do { + finished_task = g_queue_pop_head (&backlight->tasks); + + if (error) + g_task_return_error (finished_task, g_error_copy (error)); + else + g_task_return_int (finished_task, percent); + + g_object_unref (finished_task); + } while (finished_task != task); +} + +static void +gsd_backlight_set_helper_finish (GObject *obj, GAsyncResult *res, gpointer user_data) +{ + g_autoptr(GSubprocess) proc = G_SUBPROCESS (obj); + GTask *task = G_TASK (user_data); + BacklightHelperData *data = g_task_get_task_data (task); + GsdBacklight *backlight = g_task_get_source_object (task); + g_autoptr(GError) error = NULL; + + g_assert (task == backlight->active_task); + backlight->active_task = NULL; + + g_subprocess_wait_finish (proc, res, &error); + + if (error) + goto done; + + g_spawn_check_exit_status (g_subprocess_get_exit_status (proc), &error); + if (error) + goto done; + +done: + gsd_backlight_set_helper_return (backlight, task, data->value, error); + /* Start processing any tasks that were added in the meantime. */ + gsd_backlight_process_taskqueue (backlight); +} + +static void +gsd_backlight_run_set_helper (GsdBacklight *backlight, GTask *task) +{ + GSubprocess *proc = NULL; + BacklightHelperData *data = g_task_get_task_data (task); + const gchar *gsd_backlight_helper = NULL; + GError *error = NULL; + + g_assert (backlight->active_task == NULL); + backlight->active_task = task; + + if (data->value_str == NULL) + data->value_str = g_strdup_printf ("%d", data->value); + + /* This is solely for use by the test environment. If given, execute + * this helper instead of the internal helper using pkexec */ + gsd_backlight_helper = g_getenv ("GSD_BACKLIGHT_HELPER"); + if (!gsd_backlight_helper) { + proc = g_subprocess_new (G_SUBPROCESS_FLAGS_STDOUT_SILENCE, + &error, + "pkexec", + LIBEXECDIR "/gsd-backlight-helper", + g_udev_device_get_sysfs_path (backlight->udev_device), + data->value_str, NULL); + } else { + proc = g_subprocess_new (G_SUBPROCESS_FLAGS_STDOUT_SILENCE, + &error, + gsd_backlight_helper, + g_udev_device_get_sysfs_path (backlight->udev_device), + data->value_str, NULL); + } + + if (proc == NULL) { + gsd_backlight_set_helper_return (backlight, task, -1, error); + return; + } + + g_subprocess_wait_async (proc, g_task_get_cancellable (task), + gsd_backlight_set_helper_finish, + task); +} + +static void +gsd_backlight_process_taskqueue (GsdBacklight *backlight) +{ + GTask *to_run; + + /* There is already a task active, nothing to do. */ + if (backlight->active_task) + return; + + /* Get the last added task, thereby compressing the updates into one. */ + to_run = G_TASK (g_queue_peek_tail (&backlight->tasks)); + if (to_run == NULL) + return; + + /* And run it! */ + gsd_backlight_run_set_helper (backlight, to_run); +} +#endif /* __linux__ */ + +static GnomeRROutput* +gsd_backlight_rr_find_output (GsdBacklight *backlight, gboolean controllable) +{ + GnomeRROutput *output = NULL; + GnomeRROutput **outputs; + guint i; + + /* search all X11 outputs for the device id */ + outputs = gnome_rr_screen_list_outputs (backlight->rr_screen); + if (outputs == NULL) + goto out; + + for (i = 0; outputs[i] != NULL; i++) { + gboolean builtin = gnome_rr_output_is_builtin_display (outputs[i]); + gint backlight = gnome_rr_output_get_backlight (outputs[i]); + + g_debug("Output %d: %s, backlight %d", i, builtin ? "builtin" : "external", backlight); + if (builtin && (!controllable || backlight >= 0)) { + output = outputs[i]; + break; + } + } +out: + return output; +} + +/** + * gsd_backlight_get_brightness + * @backlight: a #GsdBacklight + * @target: Output parameter for the value the target value of pending set operations. + * + * The backlight value returns the last known stable value. This value will + * only update once all pending operations to set a new value have finished. + * + * As such, this function may return a different value from the return value + * of the async brightness setter. This happens when another set operation was + * queued after it was already running. + * + * If the internal display is detected as disabled, then the function will + * instead return -1. + * + * Returns: The last stable backlight value or -1 if the internal display is disabled. + **/ +gint +gsd_backlight_get_brightness (GsdBacklight *backlight, gint *target) +{ + if (backlight->builtin_display_disabled) + return -1; + + if (target) + *target = ABS_TO_PERCENTAGE (backlight->brightness_min, backlight->brightness_max, backlight->brightness_target); + + return ABS_TO_PERCENTAGE (backlight->brightness_min, backlight->brightness_max, backlight->brightness_val); +} + +static void +gsd_backlight_set_brightness_val_async (GsdBacklight *backlight, + int value, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GError *error = NULL; + GTask *task = NULL; + GnomeRROutput *output; + gint percent; + + value = MIN(backlight->brightness_max, value); + value = MAX(backlight->brightness_min, value); + + backlight->brightness_target = value; + + task = g_task_new (backlight, cancellable, callback, user_data); + +#ifdef __linux__ + if (backlight->udev_device != NULL) { + BacklightHelperData *task_data; + + if (backlight->logind_proxy) { + g_dbus_proxy_call (backlight->logind_proxy, + "SetBrightness", + g_variant_new ("(ssu)", + "backlight", + g_udev_device_get_name (backlight->udev_device), + backlight->brightness_target), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, + NULL, NULL); + + percent = ABS_TO_PERCENTAGE (backlight->brightness_min, + backlight->brightness_max, + backlight->brightness_target); + g_task_return_int (task, percent); + } else { + task_data = g_new0 (BacklightHelperData, 1); + task_data->value = backlight->brightness_target; + g_task_set_task_data (task, task_data, backlight_task_data_destroy); + + /* Task is set up now. Queue it and ensure we are working something. */ + g_queue_push_tail (&backlight->tasks, task); + gsd_backlight_process_taskqueue (backlight); + } + + return; + } +#endif /* __linux__ */ + + /* Fallback to setting via GNOME RR/X11 */ + output = gsd_backlight_rr_find_output (backlight, TRUE); + if (output) { + if (!gnome_rr_output_set_backlight (output, value, &error)) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + backlight->brightness_val = gnome_rr_output_get_backlight (output); + g_object_notify_by_pspec (G_OBJECT (backlight), props[PROP_BRIGHTNESS]); + g_task_return_int (task, gsd_backlight_get_brightness (backlight, NULL)); + g_object_unref (task); + + return; + } + + g_assert_not_reached (); + + g_task_return_new_error (task, GSD_POWER_MANAGER_ERROR, + GSD_POWER_MANAGER_ERROR_FAILED, + "No method to set brightness!"); + g_object_unref (task); +} + +void +gsd_backlight_set_brightness_async (GsdBacklight *backlight, + gint percent, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + /* Overflow/underflow is handled by gsd_backlight_set_brightness_val_async. */ + gsd_backlight_set_brightness_val_async (backlight, + PERCENTAGE_TO_ABS (backlight->brightness_min, backlight->brightness_max, percent), + cancellable, + callback, + user_data); +} + +/** + * gsd_backlight_set_brightness_finish + * @backlight: a #GsdBacklight + * @res: the #GAsyncResult passed to the callback + * @error: #GError return address + * + * Finish an operation started by gsd_backlight_set_brightness_async(). Will + * return the value that was actually set (which may be different because of + * rounding or as multiple set actions were queued up). + * + * Please note that a call to gsd_backlight_get_brightness() may not in fact + * return the same value if further operations to set the value are pending. + * + * Returns: The brightness in percent that was set. + **/ +gint +gsd_backlight_set_brightness_finish (GsdBacklight *backlight, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_int (G_TASK (res), error); +} + +void +gsd_backlight_step_up_async (GsdBacklight *backlight, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + gint value; + + /* Overflows are handled by gsd_backlight_set_brightness_val_async. */ + value = backlight->brightness_target + backlight->brightness_step; + + gsd_backlight_set_brightness_val_async (backlight, + value, + cancellable, + callback, + user_data); +} + +/** + * gsd_backlight_step_up_finish + * @backlight: a #GsdBacklight + * @res: the #GAsyncResult passed to the callback + * @error: #GError return address + * + * Finish an operation started by gsd_backlight_step_up_async(). Will return + * the value that was actually set (which may be different because of rounding + * or as multiple set actions were queued up). + * + * Please note that a call to gsd_backlight_get_brightness() may not in fact + * return the same value if further operations to set the value are pending. + * + * For simplicity it is also valid to call gsd_backlight_set_brightness_finish() + * allowing sharing the callback routine for calls to + * gsd_backlight_set_brightness_async(), gsd_backlight_step_up_async(), + * gsd_backlight_step_down_async() and gsd_backlight_cycle_up_async(). + * + * Returns: The brightness in percent that was set. + **/ +gint +gsd_backlight_step_up_finish (GsdBacklight *backlight, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_int (G_TASK (res), error); +} + +void +gsd_backlight_step_down_async (GsdBacklight *backlight, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + gint value; + + /* Underflows are handled by gsd_backlight_set_brightness_val_async. */ + value = backlight->brightness_target - backlight->brightness_step; + + gsd_backlight_set_brightness_val_async (backlight, + value, + cancellable, + callback, + user_data); +} + +/** + * gsd_backlight_step_down_finish + * @backlight: a #GsdBacklight + * @res: the #GAsyncResult passed to the callback + * @error: #GError return address + * + * Finish an operation started by gsd_backlight_step_down_async(). Will return + * the value that was actually set (which may be different because of rounding + * or as multiple set actions were queued up). + * + * Please note that a call to gsd_backlight_get_brightness() may not in fact + * return the same value if further operations to set the value are pending. + * + * For simplicity it is also valid to call gsd_backlight_set_brightness_finish() + * allowing sharing the callback routine for calls to + * gsd_backlight_set_brightness_async(), gsd_backlight_step_up_async(), + * gsd_backlight_step_down_async() and gsd_backlight_cycle_up_async(). + * + * Returns: The brightness in percent that was set. + **/ +gint +gsd_backlight_step_down_finish (GsdBacklight *backlight, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_int (G_TASK (res), error); +} + +/** + * gsd_backlight_cycle_up_async + * @backlight: a #GsdBacklight + * @cancellable: an optional #GCancellable, NULL to ignore + * @callback: the #GAsyncReadyCallback invoked for cycle up to be finished + * @user_data: the #gpointer passed to the callback + * + * Start a brightness cycle up operation by gsd_backlight_cycle_up_async(). + * The brightness will be stepped up if it is not already at the maximum. + * If it is already at the maximum, it will be set to the minimum brightness. + **/ +void +gsd_backlight_cycle_up_async (GsdBacklight *backlight, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + if (backlight->brightness_target < backlight->brightness_max) + gsd_backlight_step_up_async (backlight, + cancellable, + callback, + user_data); + else + gsd_backlight_set_brightness_val_async (backlight, + backlight->brightness_min, + cancellable, + callback, + user_data); +} + +/** + * gsd_backlight_cycle_up_finish + * @backlight: a #GsdBacklight + * @res: the #GAsyncResult passed to the callback + * @error: #GError return address + * + * Finish an operation started by gsd_backlight_cycle_up_async(). Will return + * the value that was actually set (which may be different because of rounding + * or as multiple set actions were queued up). + * + * Please note that a call to gsd_backlight_get_brightness() may not in fact + * return the same value if further operations to set the value are pending. + * + * For simplicity it is also valid to call gsd_backlight_set_brightness_finish() + * allowing sharing the callback routine for calls to + * gsd_backlight_set_brightness_async(), gsd_backlight_step_up_async(), + * gsd_backlight_step_down_async() and gsd_backlight_cycle_up_async(). + * + * Returns: The brightness in percent that was set. + **/ +gint +gsd_backlight_cycle_up_finish (GsdBacklight *backlight, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_int (G_TASK (res), error); +} + +/** + * gsd_backlight_get_connector + * @backlight: a #GsdBacklight + * + * Return the connector for the display that is being controlled by the + * #GsdBacklight object. This connector can be passed to gnome-shell to show + * the on screen display only on the affected screen. + * + * Returns: The connector of the controlled output or NULL if unknown. + **/ +const char* +gsd_backlight_get_connector (GsdBacklight *backlight) +{ + GnomeRROutput *output; + + output = gsd_backlight_rr_find_output (backlight, FALSE); + if (output == NULL) + return NULL; + + return gnome_rr_output_get_name (output); +} + +static void +gsd_backlight_rr_screen_changed_cb (GnomeRRScreen *screen, + gpointer data) +{ + GsdBacklight *backlight = GSD_BACKLIGHT (data); + GnomeRROutput *output; + gboolean builtin_display_disabled = FALSE; + + /* NOTE: Err on the side of assuming the backlight controlls something + * even if we cannot find the output that belongs to it. + * This might backfire on us obviously if the hardware claims it + * can control a non-existing screen. + */ + output = gsd_backlight_rr_find_output (backlight, FALSE); + if (output) + builtin_display_disabled = !gnome_rr_output_get_crtc (output); + + if (builtin_display_disabled != backlight->builtin_display_disabled) { + backlight->builtin_display_disabled = builtin_display_disabled; + g_object_notify_by_pspec (G_OBJECT (backlight), props[PROP_BRIGHTNESS]); + } +} + +static void +gsd_backlight_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsdBacklight *backlight = GSD_BACKLIGHT (object); + + switch (prop_id) { + case PROP_RR_SCREEN: + g_value_set_object (value, backlight->rr_screen); + break; + + case PROP_BRIGHTNESS: + g_value_set_int (value, gsd_backlight_get_brightness (backlight, NULL)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gsd_backlight_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsdBacklight *backlight = GSD_BACKLIGHT (object); + + switch (prop_id) { + case PROP_RR_SCREEN: + backlight->rr_screen = g_value_dup_object (value); + + g_signal_connect_object (backlight->rr_screen, "changed", + G_CALLBACK (gsd_backlight_rr_screen_changed_cb), + object, 0); + gsd_backlight_rr_screen_changed_cb (backlight->rr_screen, object); + + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static gboolean +gsd_backlight_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + GsdBacklight *backlight = GSD_BACKLIGHT (initable); + GnomeRROutput* output = NULL; + GError *logind_error = NULL; + + if (cancellable != NULL) { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "GsdBacklight does not support cancelling initialization."); + return FALSE; + } + +#ifdef __linux__ + backlight->logind_proxy = + g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + 0, + NULL, + SYSTEMD_DBUS_NAME, + SYSTEMD_DBUS_PATH, + SYSTEMD_DBUS_INTERFACE, + NULL, &logind_error); + if (backlight->logind_proxy) { + /* Check that the SetBrightness method does exist */ + g_dbus_proxy_call_sync (backlight->logind_proxy, + "SetBrightness", NULL, + G_DBUS_CALL_FLAGS_NONE, -1, + NULL, &logind_error); + + if (g_error_matches (logind_error, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS)) { + /* We are calling the method with no arguments, so + * this is expected. + */ + g_clear_error (&logind_error); + } else if (g_error_matches (logind_error, G_DBUS_ERROR, + G_DBUS_ERROR_UNKNOWN_METHOD)) { + /* systemd version is too old, so ignore. + */ + g_clear_error (&logind_error); + g_clear_object (&backlight->logind_proxy); + } else { + /* Fail on anything else */ + g_clear_object (&backlight->logind_proxy); + } + } + + if (logind_error) { + g_warning ("No logind found: %s", logind_error->message); + g_error_free (logind_error); + } + + /* Try finding a udev device. */ + if (gsd_backlight_udev_init (backlight)) + goto found; +#endif /* __linux__ */ + + /* Try GNOME RR as a fallback. */ + output = gsd_backlight_rr_find_output (backlight, TRUE); + if (output) { + g_debug ("Using GNOME RR (mutter) for backlight."); + backlight->brightness_min = 1; + backlight->brightness_max = 100; + backlight->brightness_val = gnome_rr_output_get_backlight (output); + backlight->brightness_step = gnome_rr_output_get_min_backlight_step (output); + + goto found; + } + + g_debug ("No usable backlight found."); + + g_set_error_literal (error, GSD_POWER_MANAGER_ERROR, GSD_POWER_MANAGER_ERROR_NO_BACKLIGHT, + "No usable backlight could be found!"); + + return FALSE; + +found: + backlight->brightness_target = backlight->brightness_val; + backlight->brightness_step = MAX(backlight->brightness_step, BRIGHTNESS_STEP_AMOUNT(backlight->brightness_max - backlight->brightness_min + 1)); + + g_debug ("Step size for backlight is %i.", backlight->brightness_step); + + return TRUE; +} + +static void +gsd_backlight_finalize (GObject *object) +{ + GsdBacklight *backlight = GSD_BACKLIGHT (object); + +#ifdef __linux__ + g_assert (backlight->active_task == NULL); + g_assert (g_queue_is_empty (&backlight->tasks)); + g_clear_object (&backlight->logind_proxy); + g_clear_object (&backlight->udev); + g_clear_object (&backlight->udev_device); + if (backlight->idle_update) { + g_source_remove (backlight->idle_update); + backlight->idle_update = 0; + } +#endif /* __linux__ */ + + g_clear_object (&backlight->rr_screen); +} + +static void +gsd_backlight_initable_iface_init (GInitableIface *iface) +{ + iface->init = gsd_backlight_initable_init; +} + +static void +gsd_backlight_class_init (GsdBacklightClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_backlight_finalize; + object_class->get_property = gsd_backlight_get_property; + object_class->set_property = gsd_backlight_set_property; + + props[PROP_RR_SCREEN] = g_param_spec_object ("rr-screen", "GnomeRRScreen", + "GnomeRRScreen usable for backlight control.", + GNOME_TYPE_RR_SCREEN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + props[PROP_BRIGHTNESS] = g_param_spec_int ("brightness", "The display brightness", + "The brightness of the internal display in percent.", + 0, 100, 100, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST, props); +} + + +static void +gsd_backlight_init (GsdBacklight *backlight) +{ + backlight->brightness_target = -1; + backlight->brightness_min = -1; + backlight->brightness_max = -1; + backlight->brightness_val = -1; + backlight->brightness_step = 1; + +#ifdef __linux__ + backlight->active_task = NULL; + g_queue_init (&backlight->tasks); +#endif /* __linux__ */ +} + +GsdBacklight * +gsd_backlight_new (GnomeRRScreen *rr_screen, + GError **error) +{ + return GSD_BACKLIGHT (g_initable_new (GSD_TYPE_BACKLIGHT, NULL, error, + "rr-screen", rr_screen, + NULL)); +} + diff --git a/plugins/power/gsd-backlight.h b/plugins/power/gsd-backlight.h new file mode 100644 index 0000000..e4fac6b --- /dev/null +++ b/plugins/power/gsd-backlight.h @@ -0,0 +1,81 @@ +/* -*- mode: c; style: linux -*- + * + * Copyright (C) 2017 Red Hat, Inc. + * + * Written by: Benjamin Berg <bberg@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef _GSD_BACKLIGHT_H +#define _GSD_BACKLIGHT_H + +#include <glib.h> +#include <gio/gio.h> + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include <libgnome-desktop/gnome-rr.h> + + +G_BEGIN_DECLS + +#define GSD_TYPE_BACKLIGHT gsd_backlight_get_type () +G_DECLARE_FINAL_TYPE (GsdBacklight, gsd_backlight, GSD, BACKLIGHT, GObject); + +gint gsd_backlight_get_brightness (GsdBacklight *backlight, + gint *target); + +void gsd_backlight_set_brightness_async (GsdBacklight *backlight, + gint percentage, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +void gsd_backlight_step_up_async (GsdBacklight *backlight, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +void gsd_backlight_step_down_async (GsdBacklight *backlight, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +void gsd_backlight_cycle_up_async (GsdBacklight *backlight, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gint gsd_backlight_set_brightness_finish (GsdBacklight *backlight, + GAsyncResult *res, + GError **error); + +gint gsd_backlight_step_up_finish (GsdBacklight *backlight, + GAsyncResult *res, + GError **error); + +gint gsd_backlight_step_down_finish (GsdBacklight *backlight, + GAsyncResult *res, + GError **error); + +gint gsd_backlight_cycle_up_finish (GsdBacklight *backlight, + GAsyncResult *res, + GError **error); + +const char* gsd_backlight_get_connector (GsdBacklight *backlight); + +GsdBacklight* gsd_backlight_new (GnomeRRScreen *screen, + GError **error); + + +G_END_DECLS + +#endif /* _GSD_BACKLIGHT_H */ diff --git a/plugins/power/gsd-power-constants-update.pl b/plugins/power/gsd-power-constants-update.pl new file mode 100755 index 0000000..1615131 --- /dev/null +++ b/plugins/power/gsd-power-constants-update.pl @@ -0,0 +1,49 @@ +#!/usr/bin/env perl + +# Author : Simos Xenitellis <simos at gnome dot org>. +# Author : Bastien Nocera <hadess@hadess.net> +# Version : 1.2 +# +# Input : gsd-power-constants.h +# Output : gsdpowerconstants.py +# +use strict; + +# Used for reading the keysymdef symbols. +my @constantselements; + +(scalar @ARGV >= 2) or die "Usage: $0 <input> <output>\n"; +my ($input, $output) = @ARGV; + +die "Could not open file gsd-power-constants.h: $!\n" unless open(IN_CONSTANTS, "<:utf8", $input); + +# Output: gtk+/gdk/gdkkeysyms.h +die "Could not open file gsdpowerconstants.py: $!\n" unless open(OUT_CONSTANTS, ">:utf8", $output); + +print OUT_CONSTANTS<<EOF; + +# File auto-generated from script http://git.gnome.org/browse/gnome-settings-daemon/tree/plugins/power/gsd-power-constants-update.pl + +# Modified by the GTK+ Team and others 1997-2012. See the AUTHORS +# file for a list of people on the GTK+ Team. See the ChangeLog +# files for a list of changes. These files are distributed with +# GTK+ at ftp://ftp.gtk.org/pub/gtk/. + +EOF + +while (<IN_CONSTANTS>) +{ + next if ( ! /^#define / ); + + @constantselements = split(/\s+/); + die "Internal error, no \@constantselements: $_\n" unless @constantselements; + + my $constant = $constantselements[1]; + my $value = $constantselements[2]; + + printf OUT_CONSTANTS "%s = %s;\n", $constant, $value; +} + +close IN_CONSTANTS; + +printf "We just finished converting $input to $output\nThank you\n"; diff --git a/plugins/power/gsd-power-constants.h b/plugins/power/gsd-power-constants.h new file mode 100644 index 0000000..91e2296 --- /dev/null +++ b/plugins/power/gsd-power-constants.h @@ -0,0 +1,42 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +/* The blank delay when the screensaver is active */ +#define SCREENSAVER_TIMEOUT_BLANK 30 /* seconds */ + +/* The dim delay when dimming on idle is requested but idle-delay + * is set to "Never" */ +#define IDLE_DIM_BLANK_DISABLED_MIN 60 /* seconds */ + +/* Which fraction of the idle-delay is the idle-dim delay */ +#define IDLE_DELAY_TO_IDLE_DIM_MULTIPLIER 4.0/5.0 + +/* The dim delay under which we do not bother dimming */ +#define MINIMUM_IDLE_DIM_DELAY 10 /* seconds */ + +/* The amount of time we'll undim if the machine is idle when plugged in */ +#define POWER_UP_TIME_ON_AC 15 /* seconds */ + +/* Default brightness values for the mock backlight used in the test suite */ +#define GSD_MOCK_DEFAULT_BRIGHTNESS 50 +#define GSD_MOCK_MAX_BRIGHTNESS 100 + +/* When unplugging the external monitor, give a certain amount + * of time before suspending the laptop */ +#define LID_CLOSE_SAFETY_TIMEOUT 8 /* seconds */ diff --git a/plugins/power/gsd-power-enums-update.c b/plugins/power/gsd-power-enums-update.c new file mode 100644 index 0000000..546e494 --- /dev/null +++ b/plugins/power/gsd-power-enums-update.c @@ -0,0 +1,44 @@ +#include <glib-object.h> +#include <gsd-power-enums.h> +#include <stdio.h> + +/* XXX: The following functions use printf, because otherwise there were + * build failures when the building with the memory sanitizer enabled. + * These may be false positives though. + */ + +static void +output_enum_values (GType class_type) +{ + GEnumClass *eclass; + guint i; + + eclass = G_ENUM_CLASS (g_type_class_peek (class_type)); + for (i = 0; i < eclass->n_values; i++) { + GEnumValue *value = &(eclass->values[i]); + printf ("%s = %d;\n", value->value_name, value->value); + } +} + +static void +output_flags_values (GType class_type) +{ + GFlagsClass *fclass; + guint i; + + fclass = G_FLAGS_CLASS (g_type_class_peek (class_type)); + for (i = 0; i < fclass->n_values; i++) { + GFlagsValue *value = &(fclass->values[i]); + printf ("%s = %d;\n", value->value_name, value->value); + } +} + +int +main (int argc, char **argv) +{ + g_type_class_ref (GSD_POWER_TYPE_INHIBITOR_FLAG); + g_type_class_ref (GSD_POWER_TYPE_PRESENCE_STATUS); + output_flags_values (GSD_POWER_TYPE_INHIBITOR_FLAG); + output_enum_values (GSD_POWER_TYPE_PRESENCE_STATUS); + return 0; +} diff --git a/plugins/power/gsd-power-enums.c.in b/plugins/power/gsd-power-enums.c.in new file mode 100644 index 0000000..1b994bb --- /dev/null +++ b/plugins/power/gsd-power-enums.c.in @@ -0,0 +1,40 @@ +/*** BEGIN file-header ***/ +#include "gsd-power-enums.h" +#include <glib-object.h> + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +/* enumerations from "@basename@" */ +#include "@basename@" + +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType +@enum_name@_get_type (void) +{ + static GType the_type = 0; + + if (the_type == 0) + { + static const G@Type@Value values[] = { +/*** END value-header ***/ + +/*** BEGIN value-production ***/ + { @VALUENAME@, + "@VALUENAME@", + "@valuenick@" }, +/*** END value-production ***/ + +/*** BEGIN value-tail ***/ + { 0, NULL, NULL } + }; + the_type = g_@type@_register_static ( + g_intern_static_string ("@EnumName@"), + values); + } + return the_type; +} + +/*** END value-tail ***/ diff --git a/plugins/power/gsd-power-enums.h.in b/plugins/power/gsd-power-enums.h.in new file mode 100644 index 0000000..53e37c0 --- /dev/null +++ b/plugins/power/gsd-power-enums.h.in @@ -0,0 +1,27 @@ +/*** BEGIN file-header ***/ +#ifndef GSD_POWER_ENUMS_H +#define GSD_POWER_ENUMS_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +/* Enumerations from "@basename@" */ + +/*** END file-production ***/ + +/*** BEGIN enumeration-production ***/ +#define GSD_POWER_TYPE_@ENUMSHORT@ (@enum_name@_get_type()) +GType @enum_name@_get_type (void) G_GNUC_CONST; + +/*** END enumeration-production ***/ + +/*** BEGIN file-tail ***/ +G_END_DECLS + +#endif /* !GSD_POWER_ENUMS_H */ +/*** END file-tail ***/ + diff --git a/plugins/power/gsd-power-manager.c b/plugins/power/gsd-power-manager.c new file mode 100644 index 0000000..c93c620 --- /dev/null +++ b/plugins/power/gsd-power-manager.c @@ -0,0 +1,3270 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2011-2012, 2015 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2011 Ritesh Khadgaray <khadgaray@gmail.com> + * Copyright (C) 2012-2013 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <string.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <libupower-glib/upower.h> +#include <libnotify/notify.h> +#include <canberra-gtk.h> +#include <glib-unix.h> +#include <gio/gunixfdlist.h> + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include <libgnome-desktop/gnome-rr.h> +#include <libgnome-desktop/gnome-idle-monitor.h> + +#include <gsd-input-helper.h> + +#include "gsd-power-constants.h" +#include "gsm-inhibitor-flag.h" +#include "gsm-presence-flag.h" +#include "gsm-manager-logout-mode.h" +#include "gpm-common.h" +#include "gsd-backlight.h" +#include "gnome-settings-profile.h" +#include "gnome-settings-bus.h" +#include "gsd-enums.h" +#include "gsd-power-manager.h" + +#define GSD_DBUS_NAME "org.gnome.SettingsDaemon" +#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon" +#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon" + +#define UPOWER_DBUS_NAME "org.freedesktop.UPower" +#define UPOWER_DBUS_PATH "/org/freedesktop/UPower" +#define UPOWER_DBUS_PATH_KBDBACKLIGHT "/org/freedesktop/UPower/KbdBacklight" +#define UPOWER_DBUS_INTERFACE "org.freedesktop.UPower" +#define UPOWER_DBUS_INTERFACE_KBDBACKLIGHT "org.freedesktop.UPower.KbdBacklight" + +#define GSD_POWER_SETTINGS_SCHEMA "org.gnome.settings-daemon.plugins.power" + +#define GSD_POWER_DBUS_NAME GSD_DBUS_NAME ".Power" +#define GSD_POWER_DBUS_PATH GSD_DBUS_PATH "/Power" +#define GSD_POWER_DBUS_INTERFACE GSD_DBUS_BASE_INTERFACE ".Power" +#define GSD_POWER_DBUS_INTERFACE_SCREEN GSD_POWER_DBUS_INTERFACE ".Screen" +#define GSD_POWER_DBUS_INTERFACE_KEYBOARD GSD_POWER_DBUS_INTERFACE ".Keyboard" + +#define GSD_POWER_MANAGER_NOTIFY_TIMEOUT_SHORT 10 * 1000 /* ms */ +#define GSD_POWER_MANAGER_NOTIFY_TIMEOUT_LONG 30 * 1000 /* ms */ + +#define SYSTEMD_DBUS_NAME "org.freedesktop.login1" +#define SYSTEMD_DBUS_PATH "/org/freedesktop/login1" +#define SYSTEMD_DBUS_INTERFACE "org.freedesktop.login1.Manager" + +/* Time between notifying the user about a critical action and the action itself in UPower. */ +#define GSD_ACTION_DELAY 20 +/* And the time before we stop the warning sound */ +#define GSD_STOP_SOUND_DELAY GSD_ACTION_DELAY - 2 + +/* The bandwidth of the low-pass filter used to smooth ambient light readings, + * measured in Hz. Smaller numbers result in smoother backlight changes. + * Larger numbers are more responsive to abrupt changes in ambient light. */ +#define GSD_AMBIENT_BANDWIDTH_HZ 0.1f + +/* Convert bandwidth to time constant. Units of constant are microseconds. */ +#define GSD_AMBIENT_TIME_CONSTANT (G_USEC_PER_SEC * 1.0f / (2.0f * G_PI * GSD_AMBIENT_BANDWIDTH_HZ)) + +static const gchar introspection_xml[] = +"<node>" +" <interface name='org.gnome.SettingsDaemon.Power.Screen'>" +" <property name='Brightness' type='i' access='readwrite'/>" +" <method name='StepUp'>" +" <arg type='i' name='new_percentage' direction='out'/>" +" <arg type='s' name='connector' direction='out'/>" +" </method>" +" <method name='StepDown'>" +" <arg type='i' name='new_percentage' direction='out'/>" +" <arg type='s' name='connector' direction='out'/>" +" </method>" +" <method name='Cycle'>" +" <arg type='i' name='new_percentage' direction='out'/>" +" <arg type='i' name='output_id' direction='out'/>" +" </method>" +" </interface>" +" <interface name='org.gnome.SettingsDaemon.Power.Keyboard'>" +" <property name='Brightness' type='i' access='readwrite'/>" +" <method name='StepUp'>" +" <arg type='i' name='new_percentage' direction='out'/>" +" </method>" +" <method name='StepDown'>" +" <arg type='i' name='new_percentage' direction='out'/>" +" </method>" +" <method name='Toggle'>" +" <arg type='i' name='new_percentage' direction='out'/>" +" </method>" +" <signal name='BrightnessChanged'>" +" <arg name='brightness' type='i'/>" +" <arg name='source' type='s'/>" +" </signal>" +" </interface>" +"</node>"; + +typedef enum { + GSD_POWER_IDLE_MODE_NORMAL, + GSD_POWER_IDLE_MODE_DIM, + GSD_POWER_IDLE_MODE_BLANK, + GSD_POWER_IDLE_MODE_SLEEP +} GsdPowerIdleMode; + +struct _GsdPowerManager +{ + GObject parent; + + /* D-Bus */ + GsdSessionManager *session; + guint name_id; + GDBusNodeInfo *introspection_data; + GDBusConnection *connection; + GCancellable *cancellable; + + /* Settings */ + GSettings *settings; + GSettings *settings_bus; + GSettings *settings_screensaver; + + /* Screensaver */ + GsdScreenSaver *screensaver_proxy; + gboolean screensaver_active; + + /* State */ + gboolean lid_is_present; + gboolean lid_is_closed; + gboolean session_is_active; + UpClient *up_client; + GPtrArray *devices_array; + UpDevice *device_composite; + GnomeRRScreen *rr_screen; + NotifyNotification *notification_ups_discharging; + NotifyNotification *notification_low; + NotifyNotification *notification_sleep_warning; + GsdPowerActionType sleep_action_type; + GHashTable *devices_notified_ht; /* key = serial str, value = UpDeviceLevel */ + gboolean battery_is_low; /* laptop battery low, or UPS discharging */ + + /* Brightness */ + GsdBacklight *backlight; + gint pre_dim_brightness; /* level, not percentage */ + + /* Keyboard */ + GDBusProxy *upower_kbd_proxy; + gint kbd_brightness_now; + gint kbd_brightness_max; + gint kbd_brightness_old; + gint kbd_brightness_pre_dim; + + /* Ambient */ + GDBusProxy *iio_proxy; + guint iio_proxy_watch_id; + gboolean ambient_norm_required; + gdouble ambient_accumulator; + gdouble ambient_norm_value; + gdouble ambient_percentage_old; + gdouble ambient_last_absolute; + gint64 ambient_last_time; + + /* Sound */ + guint32 critical_alert_timeout_id; + + /* systemd stuff */ + GDBusProxy *logind_proxy; + gint inhibit_lid_switch_fd; + gboolean inhibit_lid_switch_taken; + gint inhibit_suspend_fd; + gboolean inhibit_suspend_taken; + guint inhibit_lid_switch_timer_id; + gboolean is_virtual_machine; + + /* Idles */ + GnomeIdleMonitor *idle_monitor; + guint idle_dim_id; + guint idle_blank_id; + guint idle_sleep_warning_id; + guint idle_sleep_id; + guint user_active_id; + GsdPowerIdleMode current_idle_mode; + + guint temporary_unidle_on_ac_id; + GsdPowerIdleMode previous_idle_mode; + + guint xscreensaver_watchdog_timer_id; +}; + +enum { + PROP_0, +}; + +static void gsd_power_manager_class_init (GsdPowerManagerClass *klass); +static void gsd_power_manager_init (GsdPowerManager *power_manager); + +static void engine_device_warning_changed_cb (UpDevice *device, GParamSpec *pspec, GsdPowerManager *manager); +static void do_power_action_type (GsdPowerManager *manager, GsdPowerActionType action_type); +static void uninhibit_lid_switch (GsdPowerManager *manager); +static void stop_inhibit_lid_switch_timer (GsdPowerManager *manager); +static void sync_lid_inhibitor (GsdPowerManager *manager); +static void main_battery_or_ups_low_changed (GsdPowerManager *manager, gboolean is_low); +static gboolean idle_is_session_inhibited (GsdPowerManager *manager, guint mask, gboolean *is_inhibited); +static void idle_triggered_idle_cb (GnomeIdleMonitor *monitor, guint watch_id, gpointer user_data); +static void idle_became_active_cb (GnomeIdleMonitor *monitor, guint watch_id, gpointer user_data); +static void iio_proxy_changed (GsdPowerManager *manager); +static void iio_proxy_changed_cb (GDBusProxy *proxy, GVariant *changed_properties, GStrv invalidated_properties, gpointer user_data); + +G_DEFINE_TYPE (GsdPowerManager, gsd_power_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +GQuark +gsd_power_manager_error_quark (void) +{ + static GQuark quark = 0; + if (!quark) + quark = g_quark_from_static_string ("gsd_power_manager_error"); + return quark; +} + +static void +notify_close_if_showing (NotifyNotification **notification) +{ + if (*notification == NULL) + return; + notify_notification_close (*notification, NULL); + g_clear_object (notification); +} + +static void +engine_device_add (GsdPowerManager *manager, UpDevice *device) +{ + UpDeviceKind kind; + + /* Batteries and UPSes are already handled through + * the composite battery */ + g_object_get (device, "kind", &kind, NULL); + if (kind == UP_DEVICE_KIND_BATTERY || + kind == UP_DEVICE_KIND_UPS || + kind == UP_DEVICE_KIND_LINE_POWER) + return; + g_ptr_array_add (manager->devices_array, g_object_ref (device)); + + g_signal_connect (device, "notify::warning-level", + G_CALLBACK (engine_device_warning_changed_cb), manager); + + engine_device_warning_changed_cb (device, NULL, manager); +} + +static gboolean +engine_coldplug (GsdPowerManager *manager) +{ + guint i; + GPtrArray *array = NULL; + UpDevice *device; + + /* add to database */ + array = up_client_get_devices2 (manager->up_client); + + for (i = 0 ; array != NULL && i < array->len ; i++) { + device = g_ptr_array_index (array, i); + engine_device_add (manager, device); + } + + g_clear_pointer (&array, g_ptr_array_unref); + + /* never repeat */ + return FALSE; +} + +static void +engine_device_added_cb (UpClient *client, UpDevice *device, GsdPowerManager *manager) +{ + engine_device_add (manager, device); +} + +static void +engine_device_removed_cb (UpClient *client, const char *object_path, GsdPowerManager *manager) +{ + guint i; + + for (i = 0; i < manager->devices_array->len; i++) { + UpDevice *device = g_ptr_array_index (manager->devices_array, i); + + if (g_strcmp0 (object_path, up_device_get_object_path (device)) == 0) { + g_ptr_array_remove_index (manager->devices_array, i); + break; + } + } +} + +static void +on_notification_closed (NotifyNotification *notification, gpointer data) +{ + g_object_unref (notification); +} + +/* See PrivacyScope in messageTray.js in gnome-shell. A notification with + * ‘system’ scope has its detailed description shown in the lock screen. ‘user’ + * scope notifications don’t (because they could contain private information). */ +typedef enum +{ + NOTIFICATION_PRIVACY_USER, + NOTIFICATION_PRIVACY_SYSTEM, +} NotificationPrivacyScope; + +static const gchar * +notification_privacy_scope_to_string (NotificationPrivacyScope scope) +{ + switch (scope) { + case NOTIFICATION_PRIVACY_USER: + return "user"; + case NOTIFICATION_PRIVACY_SYSTEM: + return "system"; + default: + g_assert_not_reached (); + } +} + +static void +create_notification (const char *summary, + const char *body, + const char *icon_name, + NotificationPrivacyScope privacy_scope, + NotifyNotification **weak_pointer_location) +{ + NotifyNotification *notification; + + notification = notify_notification_new (summary, body, icon_name); + /* TRANSLATORS: this is the notification application name */ + notify_notification_set_app_name (notification, _("Power")); + notify_notification_set_hint_string (notification, "desktop-entry", "gnome-power-panel"); + notify_notification_set_hint_string (notification, "x-gnome-privacy-scope", + notification_privacy_scope_to_string (privacy_scope)); + notify_notification_set_urgency (notification, + NOTIFY_URGENCY_CRITICAL); + *weak_pointer_location = notification; + g_object_add_weak_pointer (G_OBJECT (notification), + (gpointer *) weak_pointer_location); + g_signal_connect (notification, "closed", + G_CALLBACK (on_notification_closed), NULL); +} + +static void +engine_ups_discharging (GsdPowerManager *manager, UpDevice *device) +{ + const gchar *title; + gchar *remaining_text = NULL; + gdouble percentage; + char *icon_name; + gint64 time_to_empty; + GString *message; + UpDeviceKind kind; + + /* get device properties */ + g_object_get (device, + "kind", &kind, + "percentage", &percentage, + "time-to-empty", &time_to_empty, + "icon-name", &icon_name, + NULL); + + if (kind != UP_DEVICE_KIND_UPS) + return; + + /* only show text if there is a valid time */ + if (time_to_empty > 0) + remaining_text = gpm_get_timestring (time_to_empty); + + /* TRANSLATORS: UPS is now discharging */ + title = _("UPS Discharging"); + + message = g_string_new (""); + if (remaining_text != NULL) { + /* TRANSLATORS: tell the user how much time they have got */ + g_string_append_printf (message, _("%s of UPS backup power remaining"), + remaining_text); + } else { + g_string_append (message, _("Unknown amount of UPS backup power remaining")); + } + g_string_append_printf (message, " (%.0f%%)", percentage); + + /* close any existing notification of this class */ + notify_close_if_showing (&manager->notification_ups_discharging); + + /* create a new notification */ + create_notification (title, message->str, + icon_name, NOTIFICATION_PRIVACY_SYSTEM, + &manager->notification_ups_discharging); + notify_notification_set_timeout (manager->notification_ups_discharging, + GSD_POWER_MANAGER_NOTIFY_TIMEOUT_LONG); + notify_notification_set_hint (manager->notification_ups_discharging, + "transient", g_variant_new_boolean (TRUE)); + + notify_notification_show (manager->notification_ups_discharging, NULL); + + g_string_free (message, TRUE); + g_free (icon_name); + g_free (remaining_text); +} + +static GsdPowerActionType +manager_critical_action_get (GsdPowerManager *manager) +{ + GsdPowerActionType policy; + char *action; + + action = up_client_get_critical_action (manager->up_client); + /* We don't make the difference between HybridSleep and Hibernate */ + if (g_strcmp0 (action, "PowerOff") == 0) + policy = GSD_POWER_ACTION_SHUTDOWN; + else + policy = GSD_POWER_ACTION_HIBERNATE; + g_free (action); + return policy; +} + +static gboolean +manager_critical_action_stop_sound_cb (GsdPowerManager *manager) +{ + /* stop playing the alert as it's too late to do anything now */ + play_loop_stop (&manager->critical_alert_timeout_id); + + return FALSE; +} + +static gboolean +engine_device_debounce_warn (GsdPowerManager *manager, + UpDeviceKind kind, + UpDeviceLevel warning, + const char *serial) +{ + gpointer last_warning_ptr; + UpDeviceLevel last_warning; + gboolean ret = TRUE; + + if (!serial) + return TRUE; + + if (kind == UP_DEVICE_KIND_BATTERY || + kind == UP_DEVICE_KIND_UPS) + return TRUE; + + if (g_hash_table_lookup_extended (manager->devices_notified_ht, serial, + NULL, &last_warning_ptr)) { + last_warning = GPOINTER_TO_INT (last_warning_ptr); + + if (last_warning >= warning) + ret = FALSE; + } + + if (warning != UP_DEVICE_LEVEL_UNKNOWN) + g_hash_table_insert (manager->devices_notified_ht, + g_strdup (serial), + GINT_TO_POINTER (warning)); + + return ret; +} + +static void +engine_charge_low (GsdPowerManager *manager, UpDevice *device) +{ + const gchar *title = NULL; + gboolean ret; + gchar *message = NULL; + gchar *tmp; + gchar *remaining_text; + gdouble percentage; + guint battery_level; + char *icon_name; + gint64 time_to_empty; + UpDeviceKind kind; + + /* get device properties */ + g_object_get (device, + "kind", &kind, + "percentage", &percentage, + "time-to-empty", &time_to_empty, + "battery-level", &battery_level, + "icon-name", &icon_name, + NULL); + + if (battery_level == UP_DEVICE_LEVEL_UNKNOWN) + battery_level = UP_DEVICE_LEVEL_NONE; + + if (kind == UP_DEVICE_KIND_BATTERY) { + + /* if the user has no other batteries, drop the "Laptop" wording */ + ret = (manager->devices_array->len > 0); + if (ret) { + /* TRANSLATORS: laptop battery low, and we only have one battery */ + title = _("Battery low"); + } else { + /* TRANSLATORS: laptop battery low, and we have more than one kind of battery */ + title = _("Laptop battery low"); + } + tmp = gpm_get_timestring (time_to_empty); + remaining_text = g_strconcat ("<b>", tmp, "</b>", NULL); + g_free (tmp); + + /* TRANSLATORS: tell the user how much time they have got */ + message = g_strdup_printf (_("Approximately %s remaining (%.0f%%)"), remaining_text, percentage); + g_free (remaining_text); + + } else if (kind == UP_DEVICE_KIND_UPS) { + /* TRANSLATORS: UPS is starting to get a little low */ + title = _("UPS low"); + tmp = gpm_get_timestring (time_to_empty); + remaining_text = g_strconcat ("<b>", tmp, "</b>", NULL); + g_free (tmp); + + /* TRANSLATORS: tell the user how much time they have got */ + message = g_strdup_printf (_("Approximately %s of remaining UPS backup power (%.0f%%)"), + remaining_text, percentage); + g_free (remaining_text); + } else if (kind == UP_DEVICE_KIND_MOUSE) { + /* TRANSLATORS: mouse is getting a little low */ + title = _("Mouse battery low"); + + /* TRANSLATORS: tell user more details */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Wireless mouse is low in power (%.0f%%)"), percentage); + else + message = g_strdup_printf (_("Wireless mouse is low in power")); + + } else if (kind == UP_DEVICE_KIND_KEYBOARD) { + /* TRANSLATORS: keyboard is getting a little low */ + title = _("Keyboard battery low"); + + /* TRANSLATORS: tell user more details */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Wireless keyboard is low in power (%.0f%%)"), percentage); + else + message = g_strdup_printf (_("Wireless keyboard is low in power")); + + } else if (kind == UP_DEVICE_KIND_PDA) { + /* TRANSLATORS: PDA is getting a little low */ + title = _("PDA battery low"); + + /* TRANSLATORS: tell user more details */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("PDA is low in power (%.0f%%)"), percentage); + else + message = g_strdup_printf (_("PDA is low in power")); + + } else if (kind == UP_DEVICE_KIND_PHONE) { + /* TRANSLATORS: cell phone (mobile) is getting a little low */ + title = _("Cell phone battery low"); + + /* TRANSLATORS: tell user more details */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Cell phone is low in power (%.0f%%)"), percentage); + else + message = g_strdup_printf (_("Cell phone is low in power")); + + } else if (kind == UP_DEVICE_KIND_MEDIA_PLAYER) { + /* TRANSLATORS: media player, e.g. mp3 is getting a little low */ + title = _("Media player battery low"); + + /* TRANSLATORS: tell user more details */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Media player is low in power (%.0f%%)"), percentage); + else + message = g_strdup_printf (_("Media player is low in power")); + + } else if (kind == UP_DEVICE_KIND_TABLET) { + /* TRANSLATORS: graphics tablet, e.g. wacom is getting a little low */ + title = _("Tablet battery low"); + + /* TRANSLATORS: tell user more details */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Tablet is low in power (%.0f%%)"), percentage); + else + message = g_strdup_printf (_("Tablet is low in power")); + + } else if (kind == UP_DEVICE_KIND_COMPUTER) { + /* TRANSLATORS: computer, e.g. ipad is getting a little low */ + title = _("Attached computer battery low"); + + /* TRANSLATORS: tell user more details */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Attached computer is low in power (%.0f%%)"), percentage); + else + message = g_strdup_printf (_("Attached computer is low in power")); + } + + /* close any existing notification of this class */ + notify_close_if_showing (&manager->notification_low); + + /* create a new notification */ + create_notification (title, message, + icon_name, NOTIFICATION_PRIVACY_SYSTEM, + &manager->notification_low); + notify_notification_set_timeout (manager->notification_low, + GSD_POWER_MANAGER_NOTIFY_TIMEOUT_LONG); + notify_notification_set_hint (manager->notification_low, + "transient", g_variant_new_boolean (TRUE)); + + notify_notification_show (manager->notification_low, NULL); + + /* play the sound, using sounds from the naming spec */ + ca_context_play (ca_gtk_context_get (), 0, + CA_PROP_EVENT_ID, "battery-low", + /* TRANSLATORS: this is the sound description */ + CA_PROP_EVENT_DESCRIPTION, _("Battery is low"), NULL); + + g_free (icon_name); + g_free (message); +} + +static void +engine_charge_critical (GsdPowerManager *manager, UpDevice *device) +{ + const gchar *title = NULL; + gboolean ret; + gchar *message = NULL; + gdouble percentage; + guint battery_level; + char *icon_name; + gint64 time_to_empty; + GsdPowerActionType policy; + UpDeviceKind kind; + + /* get device properties */ + g_object_get (device, + "kind", &kind, + "percentage", &percentage, + "battery-level", &battery_level, + "time-to-empty", &time_to_empty, + "icon-name", &icon_name, + NULL); + + if (battery_level == UP_DEVICE_LEVEL_UNKNOWN) + battery_level = UP_DEVICE_LEVEL_NONE; + + if (kind == UP_DEVICE_KIND_BATTERY) { + + /* if the user has no other batteries, drop the "Laptop" wording */ + ret = (manager->devices_array->len > 0); + if (ret) { + /* TRANSLATORS: laptop battery critically low, and only have one kind of battery */ + title = _("Battery critically low"); + } else { + /* TRANSLATORS: laptop battery critically low, and we have more than one type of battery */ + title = _("Laptop battery critically low"); + } + + /* we have to do different warnings depending on the policy */ + policy = manager_critical_action_get (manager); + + /* use different text for different actions */ + if (policy == GSD_POWER_ACTION_HIBERNATE) { + /* TRANSLATORS: give the user a ultimatum */ + message = g_strdup_printf (_("Computer will hibernate very soon unless it is plugged in.")); + + } else if (policy == GSD_POWER_ACTION_SHUTDOWN) { + /* TRANSLATORS: give the user a ultimatum */ + message = g_strdup_printf (_("Computer will shutdown very soon unless it is plugged in.")); + } + + } else if (kind == UP_DEVICE_KIND_UPS) { + gchar *remaining_text; + gchar *tmp; + + /* TRANSLATORS: the UPS is very low */ + title = _("UPS critically low"); + tmp = gpm_get_timestring (time_to_empty); + remaining_text = g_strconcat ("<b>", tmp, "</b>", NULL); + g_free (tmp); + + /* TRANSLATORS: give the user a ultimatum */ + message = g_strdup_printf (_("Approximately %s of remaining UPS power (%.0f%%). " + "Restore AC power to your computer to avoid losing data."), + remaining_text, percentage); + g_free (remaining_text); + } else if (kind == UP_DEVICE_KIND_MOUSE) { + /* TRANSLATORS: the mouse battery is very low */ + title = _("Mouse battery low"); + + /* TRANSLATORS: the device is just going to stop working */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Wireless mouse is very low in power (%.0f%%). " + "This device will soon stop functioning if not charged."), + percentage); + else + message = g_strdup_printf (_("Wireless mouse is very low in power. " + "This device will soon stop functioning if not charged.")); + + } else if (kind == UP_DEVICE_KIND_KEYBOARD) { + /* TRANSLATORS: the keyboard battery is very low */ + title = _("Keyboard battery low"); + + /* TRANSLATORS: the device is just going to stop working */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Wireless keyboard is very low in power (%.0f%%). " + "This device will soon stop functioning if not charged."), + percentage); + else + message = g_strdup_printf (_("Wireless keyboard is very low in power. " + "This device will soon stop functioning if not charged.")); + + } else if (kind == UP_DEVICE_KIND_PDA) { + + /* TRANSLATORS: the PDA battery is very low */ + title = _("PDA battery low"); + + /* TRANSLATORS: the device is just going to stop working */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("PDA is very low in power (%.0f%%). " + "This device will soon stop functioning if not charged."), + percentage); + else + message = g_strdup_printf (_("PDA is very low in power. " + "This device will soon stop functioning if not charged.")); + + } else if (kind == UP_DEVICE_KIND_PHONE) { + + /* TRANSLATORS: the cell battery is very low */ + title = _("Cell phone battery low"); + + /* TRANSLATORS: the device is just going to stop working */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Cell phone is very low in power (%.0f%%). " + "This device will soon stop functioning if not charged."), + percentage); + else + message = g_strdup_printf (_("Cell phone is very low in power. " + "This device will soon stop functioning if not charged.")); + + } else if (kind == UP_DEVICE_KIND_MEDIA_PLAYER) { + + /* TRANSLATORS: the cell battery is very low */ + title = _("Cell phone battery low"); + + /* TRANSLATORS: the device is just going to stop working */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Media player is very low in power (%.0f%%). " + "This device will soon stop functioning if not charged."), + percentage); + else + message = g_strdup_printf (_("Media player is very low in power. " + "This device will soon stop functioning if not charged.")); + + } else if (kind == UP_DEVICE_KIND_TABLET) { + + /* TRANSLATORS: the cell battery is very low */ + title = _("Tablet battery low"); + + /* TRANSLATORS: the device is just going to stop working */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Tablet is very low in power (%.0f%%). " + "This device will soon stop functioning if not charged."), + percentage); + else + message = g_strdup_printf (_("Tablet is very low in power. " + "This device will soon stop functioning if not charged.")); + + } else if (kind == UP_DEVICE_KIND_COMPUTER) { + + /* TRANSLATORS: the cell battery is very low */ + title = _("Attached computer battery low"); + + /* TRANSLATORS: the device is just going to stop working */ + if (battery_level == UP_DEVICE_LEVEL_NONE) + message = g_strdup_printf (_("Attached computer is very low in power (%.0f%%). " + "The device will soon shutdown if not charged."), + percentage); + else + message = g_strdup_printf (_("Attached computer is very low in power. " + "The device will soon shutdown if not charged.")); + + } + + /* close any existing notification of this class */ + notify_close_if_showing (&manager->notification_low); + + /* create a new notification */ + create_notification (title, message, + icon_name, NOTIFICATION_PRIVACY_SYSTEM, + &manager->notification_low); + notify_notification_set_timeout (manager->notification_low, + NOTIFY_EXPIRES_NEVER); + + notify_notification_show (manager->notification_low, NULL); + + switch (kind) { + + case UP_DEVICE_KIND_BATTERY: + case UP_DEVICE_KIND_UPS: + g_debug ("critical charge level reached, starting sound loop"); + play_loop_start (&manager->critical_alert_timeout_id); + break; + + default: + /* play the sound, using sounds from the naming spec */ + ca_context_play (ca_gtk_context_get (), 0, + CA_PROP_EVENT_ID, "battery-caution", + /* TRANSLATORS: this is the sound description */ + CA_PROP_EVENT_DESCRIPTION, _("Battery is critically low"), NULL); + break; + } + + g_free (icon_name); + g_free (message); +} + +static void +engine_charge_action (GsdPowerManager *manager, UpDevice *device) +{ + const gchar *title = NULL; + gchar *message = NULL; + char *icon_name; + GsdPowerActionType policy; + guint timer_id; + UpDeviceKind kind; + + /* get device properties */ + g_object_get (device, + "kind", &kind, + "icon-name", &icon_name, + NULL); + + if (kind == UP_DEVICE_KIND_BATTERY) { + + /* TRANSLATORS: laptop battery is really, really, low */ + title = _("Laptop battery critically low"); + + /* we have to do different warnings depending on the policy */ + policy = manager_critical_action_get (manager); + + /* use different text for different actions */ + if (policy == GSD_POWER_ACTION_HIBERNATE) { + /* TRANSLATORS: computer will hibernate */ + message = g_strdup (_("The battery is below the critical level and " + "this computer is about to hibernate.")); + + } else if (policy == GSD_POWER_ACTION_SHUTDOWN) { + /* TRANSLATORS: computer will just shutdown */ + message = g_strdup (_("The battery is below the critical level and " + "this computer is about to shutdown.")); + } + + /* wait 20 seconds for user-panic */ + timer_id = g_timeout_add_seconds (GSD_STOP_SOUND_DELAY, + (GSourceFunc) manager_critical_action_stop_sound_cb, + manager); + g_source_set_name_by_id (timer_id, "[GsdPowerManager] battery critical-action"); + + } else if (kind == UP_DEVICE_KIND_UPS) { + /* TRANSLATORS: UPS is really, really, low */ + title = _("UPS critically low"); + + /* we have to do different warnings depending on the policy */ + policy = manager_critical_action_get (manager); + + /* use different text for different actions */ + if (policy == GSD_POWER_ACTION_HIBERNATE) { + /* TRANSLATORS: computer will hibernate */ + message = g_strdup (_("UPS is below the critical level and " + "this computer is about to hibernate.")); + + } else if (policy == GSD_POWER_ACTION_SHUTDOWN) { + /* TRANSLATORS: computer will just shutdown */ + message = g_strdup (_("UPS is below the critical level and " + "this computer is about to shutdown.")); + } + + /* wait 20 seconds for user-panic */ + timer_id = g_timeout_add_seconds (GSD_STOP_SOUND_DELAY, + (GSourceFunc) manager_critical_action_stop_sound_cb, + manager); + g_source_set_name_by_id (timer_id, "[GsdPowerManager] ups critical-action"); + } + + /* not all types have actions */ + if (title == NULL) + return; + + /* close any existing notification of this class */ + notify_close_if_showing (&manager->notification_low); + + /* create a new notification */ + create_notification (title, message, + icon_name, NOTIFICATION_PRIVACY_SYSTEM, + &manager->notification_low); + notify_notification_set_timeout (manager->notification_low, + NOTIFY_EXPIRES_NEVER); + + /* try to show */ + notify_notification_show (manager->notification_low, NULL); + + /* play the sound, using sounds from the naming spec */ + ca_context_play (ca_gtk_context_get (), 0, + CA_PROP_EVENT_ID, "battery-caution", + /* TRANSLATORS: this is the sound description */ + CA_PROP_EVENT_DESCRIPTION, _("Battery is critically low"), NULL); + + g_free (icon_name); + g_free (message); +} + +static void +engine_device_warning_changed_cb (UpDevice *device, GParamSpec *pspec, GsdPowerManager *manager) +{ + g_autofree char *serial = NULL; + UpDeviceLevel warning; + UpDeviceKind kind; + + g_object_get (device, + "serial", &serial, + "warning-level", &warning, + "kind", &kind, + NULL); + + if (!engine_device_debounce_warn (manager, kind, warning, serial)) + return; + + if (warning == UP_DEVICE_LEVEL_DISCHARGING) { + g_debug ("** EMIT: discharging"); + engine_ups_discharging (manager, device); + } else if (warning == UP_DEVICE_LEVEL_LOW) { + g_debug ("** EMIT: charge-low"); + engine_charge_low (manager, device); + } else if (warning == UP_DEVICE_LEVEL_CRITICAL) { + g_debug ("** EMIT: charge-critical"); + engine_charge_critical (manager, device); + } else if (warning == UP_DEVICE_LEVEL_ACTION) { + g_debug ("** EMIT: charge-action"); + engine_charge_action (manager, device); + } else if (warning == UP_DEVICE_LEVEL_NONE) { + /* FIXME: this only handles one notification + * for the whole system, instead of one per device */ + g_debug ("fully charged or charging, hiding notifications if any"); + play_loop_stop (&manager->critical_alert_timeout_id); + if (kind != UP_DEVICE_KIND_UPS) + notify_close_if_showing (&manager->notification_low); + else + notify_close_if_showing (&manager->notification_ups_discharging); + } + + if (kind == UP_DEVICE_KIND_BATTERY || + kind == UP_DEVICE_KIND_UPS) + main_battery_or_ups_low_changed (manager, (warning != UP_DEVICE_LEVEL_NONE)); +} + +static void +gnome_session_shutdown_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GVariant *result; + GError *error = NULL; + + result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), + res, + &error); + if (result == NULL) { + g_warning ("couldn't shutdown using gnome-session: %s", + error->message); + g_error_free (error); + } else { + g_variant_unref (result); + } +} + +static void +gnome_session_shutdown (GsdPowerManager *manager) +{ + g_dbus_proxy_call (G_DBUS_PROXY (manager->session), + "Shutdown", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, + gnome_session_shutdown_cb, NULL); +} + +static void +gnome_session_logout_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GVariant *result; + GError *error = NULL; + + result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), + res, + &error); + if (result == NULL) { + g_warning ("couldn't log out using gnome-session: %s", + error->message); + g_error_free (error); + } else { + g_variant_unref (result); + } +} + +static void +gnome_session_logout (GsdPowerManager *manager, + guint logout_mode) +{ + if (g_getenv ("RUNNING_UNDER_GDM")) { + g_warning ("Prevented logout from GDM session! This indicates an issue in gsd-power."); + return; + } + + g_dbus_proxy_call (G_DBUS_PROXY (manager->session), + "Logout", + g_variant_new ("(u)", logout_mode), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, + gnome_session_logout_cb, NULL); +} + +static void +dbus_call_log_error (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + const gchar *msg = user_data; + + result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), + res, + &error); + if (result == NULL) + g_warning ("%s: %s", msg, error->message); +} + +static void +action_poweroff (GsdPowerManager *manager) +{ + if (manager->logind_proxy == NULL) { + g_warning ("no systemd support"); + return; + } + g_dbus_proxy_call (manager->logind_proxy, + "PowerOff", + g_variant_new ("(b)", FALSE), + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, + NULL, + dbus_call_log_error, + "Error calling PowerOff"); +} + +static void +action_suspend (GsdPowerManager *manager) +{ + if (manager->logind_proxy == NULL) { + g_warning ("no systemd support"); + return; + } + g_dbus_proxy_call (manager->logind_proxy, + "Suspend", + g_variant_new ("(b)", FALSE), + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, + NULL, + dbus_call_log_error, + "Error calling suspend action"); +} + +static void +action_hibernate (GsdPowerManager *manager) +{ + if (manager->logind_proxy == NULL) { + g_warning ("no systemd support"); + return; + } + g_dbus_proxy_call (manager->logind_proxy, + "Hibernate", + g_variant_new ("(b)", FALSE), + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, + NULL, + dbus_call_log_error, + "Error calling Hibernate"); +} + +static void +iio_proxy_claim_light (GsdPowerManager *manager, gboolean active) +{ + GError *error = NULL; + if (manager->iio_proxy == NULL) + return; + if (!manager->backlight) + return; + if (active && !manager->session_is_active) + return; + + /* FIXME: + * Remove when iio-sensor-proxy sends events only to clients instead + * of all listeners: + * https://github.com/hadess/iio-sensor-proxy/issues/210 */ + + /* disconnect, otherwise callback can be added multiple times */ + g_signal_handlers_disconnect_by_func (manager->iio_proxy, + G_CALLBACK (iio_proxy_changed_cb), + manager); + + if (active) + g_signal_connect (manager->iio_proxy, "g-properties-changed", + G_CALLBACK (iio_proxy_changed_cb), manager); + + if (!g_dbus_proxy_call_sync (manager->iio_proxy, + active ? "ClaimLight" : "ReleaseLight", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &error)) { + g_warning ("Call to iio-proxy failed: %s", error->message); + g_error_free (error); + } + + if (active) + iio_proxy_changed (manager); +} + +static void +backlight_enable (GsdPowerManager *manager) +{ + gboolean ret; + GError *error = NULL; + + iio_proxy_claim_light (manager, TRUE); + ret = gnome_rr_screen_set_dpms_mode (manager->rr_screen, + GNOME_RR_DPMS_ON, + &error); + if (!ret) { + g_warning ("failed to turn the panel on: %s", + error->message); + g_error_free (error); + } + + g_debug ("TESTSUITE: Unblanked screen"); +} + +static void +backlight_disable (GsdPowerManager *manager) +{ + gboolean ret; + GError *error = NULL; + + iio_proxy_claim_light (manager, FALSE); + ret = gnome_rr_screen_set_dpms_mode (manager->rr_screen, + GNOME_RR_DPMS_OFF, + &error); + if (!ret) { + g_warning ("failed to turn the panel off: %s", + error->message); + g_error_free (error); + } + + g_debug ("TESTSUITE: Blanked screen"); +} + +static void +do_power_action_type (GsdPowerManager *manager, + GsdPowerActionType action_type) +{ + switch (action_type) { + case GSD_POWER_ACTION_SUSPEND: + action_suspend (manager); + break; + case GSD_POWER_ACTION_INTERACTIVE: + gnome_session_shutdown (manager); + break; + case GSD_POWER_ACTION_HIBERNATE: + action_hibernate (manager); + break; + case GSD_POWER_ACTION_SHUTDOWN: + /* this is only used on critically low battery where + * hibernate is not available and is marginally better + * than just powering down the computer mid-write */ + action_poweroff (manager); + break; + case GSD_POWER_ACTION_BLANK: + backlight_disable (manager); + break; + case GSD_POWER_ACTION_NOTHING: + break; + case GSD_POWER_ACTION_LOGOUT: + gnome_session_logout (manager, GSM_MANAGER_LOGOUT_MODE_FORCE); + break; + } +} + +static GsmInhibitorFlag +get_idle_inhibitors_for_action (GsdPowerActionType action_type) +{ + switch (action_type) { + case GSD_POWER_ACTION_BLANK: + case GSD_POWER_ACTION_SHUTDOWN: + case GSD_POWER_ACTION_INTERACTIVE: + return GSM_INHIBITOR_FLAG_IDLE; + case GSD_POWER_ACTION_HIBERNATE: + case GSD_POWER_ACTION_SUSPEND: + return GSM_INHIBITOR_FLAG_SUSPEND; /* in addition to idle */ + case GSD_POWER_ACTION_NOTHING: + return 0; + case GSD_POWER_ACTION_LOGOUT: + return GSM_INHIBITOR_FLAG_LOGOUT; /* in addition to idle */ + } + return 0; +} + +static gboolean +is_action_inhibited (GsdPowerManager *manager, GsdPowerActionType action_type) +{ + GsmInhibitorFlag flag; + gboolean is_inhibited; + + flag = get_idle_inhibitors_for_action (action_type); + if (!flag) + return FALSE; + idle_is_session_inhibited (manager, + flag, + &is_inhibited); + return is_inhibited; +} + +static gboolean +upower_kbd_set_brightness (GsdPowerManager *manager, guint value, GError **error) +{ + GVariant *retval; + + /* same as before */ + if (manager->kbd_brightness_now == value) + return TRUE; + if (manager->upower_kbd_proxy == NULL) + return TRUE; + + /* update h/w value */ + retval = g_dbus_proxy_call_sync (manager->upower_kbd_proxy, + "SetBrightness", + g_variant_new ("(i)", (gint) value), + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + error); + if (retval == NULL) + return FALSE; + + /* save new value */ + manager->kbd_brightness_now = value; + g_variant_unref (retval); + return TRUE; +} + +static int +upower_kbd_toggle (GsdPowerManager *manager, + GError **error) +{ + gboolean ret; + int value = -1; + + if (manager->kbd_brightness_old >= 0) { + g_debug ("keyboard toggle off"); + ret = upower_kbd_set_brightness (manager, + manager->kbd_brightness_old, + error); + if (ret) { + /* succeeded, set to -1 since now no old value */ + manager->kbd_brightness_old = -1; + value = 0; + } + } else { + g_debug ("keyboard toggle on"); + /* save the current value to restore later when untoggling */ + manager->kbd_brightness_old = manager->kbd_brightness_now; + ret = upower_kbd_set_brightness (manager, 0, error); + if (!ret) { + /* failed, reset back to -1 */ + manager->kbd_brightness_old = -1; + } else { + value = 0; + } + } + + if (ret) + return value; + return -1; +} + +static gboolean +suspend_on_lid_close (GsdPowerManager *manager) +{ + return !external_monitor_is_connected (manager->rr_screen) || !manager->session_is_active; +} + +static gboolean +inhibit_lid_switch_timer_cb (GsdPowerManager *manager) +{ + stop_inhibit_lid_switch_timer (manager); + + if (suspend_on_lid_close (manager)) { + g_debug ("no external monitors or session inactive for a while; uninhibiting lid close"); + uninhibit_lid_switch (manager); + } + + /* This is a one shot timer. */ + return G_SOURCE_REMOVE; +} + +/* Sets up a timer to be triggered some seconds after closing the laptop lid + * when the laptop is *not* suspended for some reason. We'll check conditions + * again in the timeout handler to see if we can suspend then. + */ +static void +setup_inhibit_lid_switch_timer (GsdPowerManager *manager) +{ + if (manager->inhibit_lid_switch_timer_id != 0) { + g_debug ("lid close safety timer already set up"); + return; + } + + g_debug ("setting up lid close safety timer"); + + manager->inhibit_lid_switch_timer_id = g_timeout_add_seconds (LID_CLOSE_SAFETY_TIMEOUT, + (GSourceFunc) inhibit_lid_switch_timer_cb, + manager); + g_source_set_name_by_id (manager->inhibit_lid_switch_timer_id, "[GsdPowerManager] lid close safety timer"); +} + +static void +stop_inhibit_lid_switch_timer (GsdPowerManager *manager) { + if (manager->inhibit_lid_switch_timer_id != 0) { + g_debug ("stopping lid close safety timer"); + g_source_remove (manager->inhibit_lid_switch_timer_id); + manager->inhibit_lid_switch_timer_id = 0; + } +} + +static void +restart_inhibit_lid_switch_timer (GsdPowerManager *manager) +{ + stop_inhibit_lid_switch_timer (manager); + g_debug ("restarting lid close safety timer"); + setup_inhibit_lid_switch_timer (manager); +} + +static void +do_lid_open_action (GsdPowerManager *manager) +{ + /* play a sound, using sounds from the naming spec */ + ca_context_play (ca_gtk_context_get (), 0, + CA_PROP_EVENT_ID, "lid-open", + /* TRANSLATORS: this is the sound description */ + CA_PROP_EVENT_DESCRIPTION, _("Lid has been opened"), + NULL); +} + +static void +lock_screensaver (GsdPowerManager *manager) +{ + gboolean do_lock; + + do_lock = g_settings_get_boolean (manager->settings_screensaver, + "lock-enabled"); + if (!do_lock) { + g_dbus_proxy_call_sync (G_DBUS_PROXY (manager->screensaver_proxy), + "SetActive", + g_variant_new ("(b)", TRUE), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL); + return; + } + + g_dbus_proxy_call_sync (G_DBUS_PROXY (manager->screensaver_proxy), + "Lock", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL); +} + +static void +do_lid_closed_action (GsdPowerManager *manager) +{ + /* play a sound, using sounds from the naming spec */ + ca_context_play (ca_gtk_context_get (), 0, + CA_PROP_EVENT_ID, "lid-close", + /* TRANSLATORS: this is the sound description */ + CA_PROP_EVENT_DESCRIPTION, _("Lid has been closed"), + NULL); + + /* refresh RANDR so we get an accurate view of what monitors are plugged in when the lid is closed */ + gnome_rr_screen_refresh (manager->rr_screen, NULL); /* NULL-GError */ + + if (suspend_on_lid_close (manager)) { + gboolean is_inhibited; + + idle_is_session_inhibited (manager, + GSM_INHIBITOR_FLAG_SUSPEND, + &is_inhibited); + if (is_inhibited) { + g_debug ("Suspend is inhibited but lid is closed, locking the screen"); + /* We put the screensaver on * as we're not suspending, + * but the lid is closed */ + lock_screensaver (manager); + } + } +} + +static void +lid_state_changed_cb (UpClient *client, GParamSpec *pspec, GsdPowerManager *manager) +{ + gboolean tmp; + + if (!manager->lid_is_present) + return; + + /* same lid state */ + tmp = up_client_get_lid_is_closed (manager->up_client); + if (manager->lid_is_closed == tmp) + return; + manager->lid_is_closed = tmp; + g_debug ("up changed: lid is now %s", tmp ? "closed" : "open"); + + if (manager->lid_is_closed) + do_lid_closed_action (manager); + else + do_lid_open_action (manager); +} + +static const gchar * +idle_mode_to_string (GsdPowerIdleMode mode) +{ + if (mode == GSD_POWER_IDLE_MODE_NORMAL) + return "normal"; + if (mode == GSD_POWER_IDLE_MODE_DIM) + return "dim"; + if (mode == GSD_POWER_IDLE_MODE_BLANK) + return "blank"; + if (mode == GSD_POWER_IDLE_MODE_SLEEP) + return "sleep"; + return "unknown"; +} + +static const char * +idle_watch_id_to_string (GsdPowerManager *manager, guint id) +{ + if (id == manager->idle_dim_id) + return "dim"; + if (id == manager->idle_blank_id) + return "blank"; + if (id == manager->idle_sleep_id) + return "sleep"; + if (id == manager->idle_sleep_warning_id) + return "sleep-warning"; + return NULL; +} + +static void +backlight_iface_emit_changed (GsdPowerManager *manager, + const char *interface_name, + gint32 value, + const char *source) +{ + GVariant *params; + + /* not yet connected to the bus */ + if (manager->connection == NULL) + return; + + params = g_variant_new_parsed ("(%s, [{'Brightness', <%i>}], @as [])", interface_name, + value); + g_dbus_connection_emit_signal (manager->connection, + NULL, + GSD_POWER_DBUS_PATH, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + params, NULL); + + if (!source) + return; + + g_dbus_connection_emit_signal (manager->connection, + NULL, + GSD_POWER_DBUS_PATH, + GSD_POWER_DBUS_INTERFACE_KEYBOARD, + "BrightnessChanged", + g_variant_new ("(is)", value, source), + NULL); +} + +static void +backlight_notify_brightness_cb (GsdPowerManager *manager, GParamSpec *pspec, GsdBacklight *backlight) +{ + backlight_iface_emit_changed (manager, GSD_POWER_DBUS_INTERFACE_SCREEN, + gsd_backlight_get_brightness (backlight, NULL), NULL); +} + +static void +display_backlight_dim (GsdPowerManager *manager, + gint idle_percentage) +{ + gint brightness; + + if (!manager->backlight) + return; + + /* Fetch the current target brightness (not the actual display brightness) + * and return if it is already lower than the idle percentage. */ + gsd_backlight_get_brightness (manager->backlight, &brightness); + if (brightness < idle_percentage) + return; + + manager->pre_dim_brightness = brightness; + gsd_backlight_set_brightness_async (manager->backlight, idle_percentage, NULL, NULL, NULL); +} + +static gboolean +kbd_backlight_dim (GsdPowerManager *manager, + gint idle_percentage, + GError **error) +{ + gboolean ret; + gint idle; + gint max; + gint now; + + if (manager->upower_kbd_proxy == NULL) + return TRUE; + + now = manager->kbd_brightness_now; + max = manager->kbd_brightness_max; + idle = PERCENTAGE_TO_ABS (0, max, idle_percentage); + if (idle > now) { + g_debug ("kbd brightness already now %i/%i, so " + "ignoring dim to %i/%i", + now, max, idle, max); + return TRUE; + } + ret = upower_kbd_set_brightness (manager, idle, error); + if (!ret) + return FALSE; + + /* save for undim */ + manager->kbd_brightness_pre_dim = now; + return TRUE; +} + +static void +upower_kbd_proxy_signal_cb (GDBusProxy *proxy, + const gchar *sender_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + gint brightness, percentage; + const gchar *source; + + if (g_strcmp0 (signal_name, "BrightnessChangedWithSource") != 0) + return; + + g_variant_get (parameters, "(i&s)", &brightness, &source); + + /* Ignore changes caused by us calling UPower's SetBrightness method, + * we already call backlight_iface_emit_changed for these after the + * SetBrightness method call completes. */ + if (g_strcmp0 (source, "external") == 0) + return; + + manager->kbd_brightness_now = brightness; + percentage = ABS_TO_PERCENTAGE (0, + manager->kbd_brightness_max, + manager->kbd_brightness_now); + backlight_iface_emit_changed (manager, GSD_POWER_DBUS_INTERFACE_KEYBOARD, percentage, source); +} + +static gboolean +is_session_active (GsdPowerManager *manager) +{ + GVariant *variant; + gboolean is_session_active = FALSE; + + variant = g_dbus_proxy_get_cached_property (G_DBUS_PROXY (manager->session), + "SessionIsActive"); + if (variant) { + is_session_active = g_variant_get_boolean (variant); + g_variant_unref (variant); + } + + return is_session_active; +} + +static void +idle_set_mode (GsdPowerManager *manager, GsdPowerIdleMode mode) +{ + gboolean ret = FALSE; + GError *error = NULL; + gint idle_percentage; + GsdPowerActionType action_type; + + /* Ignore attempts to set "less idle" modes */ + if (mode <= manager->current_idle_mode && + mode != GSD_POWER_IDLE_MODE_NORMAL) { + g_debug ("Not going to 'less idle' mode %s (current: %s)", + idle_mode_to_string (mode), + idle_mode_to_string (manager->current_idle_mode)); + return; + } + + /* ensure we're still on an active console */ + if (!manager->session_is_active) { + g_debug ("ignoring state transition to %s as inactive", + idle_mode_to_string (mode)); + return; + } + + manager->current_idle_mode = mode; + g_debug ("Doing a state transition: %s", idle_mode_to_string (mode)); + + /* if we're moving to an idle mode, make sure + * we add a watch to take us back to normal */ + if (mode != GSD_POWER_IDLE_MODE_NORMAL) { + if (manager->user_active_id < 1) { + manager->user_active_id = gnome_idle_monitor_add_user_active_watch (manager->idle_monitor, + idle_became_active_cb, + manager, + NULL); + g_debug ("installing idle_became_active_cb to clear sleep warning when transitioning away from normal (%i)", + manager->user_active_id); + } + } + + /* save current brightness, and set dim level */ + if (mode == GSD_POWER_IDLE_MODE_DIM) { + /* display backlight */ + idle_percentage = g_settings_get_int (manager->settings, + "idle-brightness"); + display_backlight_dim (manager, idle_percentage); + + /* keyboard backlight */ + ret = kbd_backlight_dim (manager, idle_percentage, &error); + if (!ret) { + g_warning ("failed to set dim kbd backlight to %i%%: %s", + idle_percentage, + error->message); + g_clear_error (&error); + } + + /* turn off screen and kbd */ + } else if (mode == GSD_POWER_IDLE_MODE_BLANK) { + + backlight_disable (manager); + + /* only toggle keyboard if present and not already toggled */ + if (manager->upower_kbd_proxy && + manager->kbd_brightness_old == -1) { + if (upower_kbd_toggle (manager, &error) < 0) { + g_warning ("failed to turn the kbd backlight off: %s", + error->message); + g_error_free (error); + } + } + + /* sleep */ + } else if (mode == GSD_POWER_IDLE_MODE_SLEEP) { + + if (up_client_get_on_battery (manager->up_client)) { + action_type = g_settings_get_enum (manager->settings, + "sleep-inactive-battery-type"); + } else { + action_type = g_settings_get_enum (manager->settings, + "sleep-inactive-ac-type"); + } + do_power_action_type (manager, action_type); + + /* turn on screen and restore user-selected brightness level */ + } else if (mode == GSD_POWER_IDLE_MODE_NORMAL) { + + backlight_enable (manager); + + /* reset brightness if we dimmed */ + if (manager->backlight && manager->pre_dim_brightness >= 0) { + gsd_backlight_set_brightness_async (manager->backlight, + manager->pre_dim_brightness, + NULL, NULL, NULL); + /* XXX: Ideally we would do this from the async callback. */ + manager->pre_dim_brightness = -1; + } + + /* only toggle keyboard if present and already toggled off */ + if (manager->upower_kbd_proxy && + manager->kbd_brightness_old != -1) { + if (upower_kbd_toggle (manager, &error) < 0) { + g_warning ("failed to turn the kbd backlight on: %s", + error->message); + g_clear_error (&error); + } + } + + /* reset kbd brightness if we dimmed */ + if (manager->kbd_brightness_pre_dim >= 0) { + ret = upower_kbd_set_brightness (manager, + manager->kbd_brightness_pre_dim, + &error); + if (!ret) { + g_warning ("failed to restore kbd backlight to %i: %s", + manager->kbd_brightness_pre_dim, + error->message); + g_error_free (error); + } + manager->kbd_brightness_pre_dim = -1; + } + + } +} + +static gboolean +idle_is_session_inhibited (GsdPowerManager *manager, + GsmInhibitorFlag mask, + gboolean *is_inhibited) +{ + GVariant *variant; + GsmInhibitorFlag inhibited_actions; + + /* not yet connected to gnome-session */ + if (manager->session == NULL) + return FALSE; + + variant = g_dbus_proxy_get_cached_property (G_DBUS_PROXY (manager->session), + "InhibitedActions"); + if (!variant) + return FALSE; + + inhibited_actions = g_variant_get_uint32 (variant); + g_variant_unref (variant); + + *is_inhibited = (inhibited_actions & mask); + + return TRUE; +} + +static void +clear_idle_watch (GnomeIdleMonitor *monitor, + guint *id) +{ + if (*id == 0) + return; + gnome_idle_monitor_remove_watch (monitor, *id); + *id = 0; +} + +static void +idle_configure (GsdPowerManager *manager) +{ + gboolean is_idle_inhibited; + GsdPowerActionType action_type; + guint timeout_sleep; + guint timeout_dim; + gboolean on_battery; + + if (!idle_is_session_inhibited (manager, + GSM_INHIBITOR_FLAG_IDLE, + &is_idle_inhibited)) { + /* Session isn't available yet, postpone */ + return; + } + + /* set up blank callback only when the screensaver is on, + * as it's what will drive the blank */ + clear_idle_watch (manager->idle_monitor, + &manager->idle_blank_id); + if (manager->screensaver_active) { + /* The tail is wagging the dog. + * The screensaver coming on will blank the screen. + * If an event occurs while the screensaver is on, + * the aggressive idle watch will handle it */ + guint timeout_blank = SCREENSAVER_TIMEOUT_BLANK; + g_debug ("setting up blank callback for %is", timeout_blank); + manager->idle_blank_id = gnome_idle_monitor_add_idle_watch (manager->idle_monitor, + timeout_blank * 1000, + idle_triggered_idle_cb, manager, NULL); + } + + /* are we inhibited from going idle */ + if (!manager->session_is_active || + (is_idle_inhibited && !manager->screensaver_active)) { + if (is_idle_inhibited && !manager->screensaver_active) + g_debug ("inhibited and screensaver not active, so using normal state"); + else + g_debug ("inactive, so using normal state"); + idle_set_mode (manager, GSD_POWER_IDLE_MODE_NORMAL); + + clear_idle_watch (manager->idle_monitor, + &manager->idle_sleep_id); + clear_idle_watch (manager->idle_monitor, + &manager->idle_dim_id); + clear_idle_watch (manager->idle_monitor, + &manager->idle_sleep_warning_id); + notify_close_if_showing (&manager->notification_sleep_warning); + return; + } + + /* only do the sleep timeout when the session is idle + * and we aren't inhibited from sleeping (or logging out, etc.) */ + on_battery = up_client_get_on_battery (manager->up_client); + action_type = g_settings_get_enum (manager->settings, on_battery ? + "sleep-inactive-battery-type" : "sleep-inactive-ac-type"); + timeout_sleep = 0; + if (!is_action_inhibited (manager, action_type)) { + gint timeout_sleep_; + timeout_sleep_ = g_settings_get_int (manager->settings, on_battery ? + "sleep-inactive-battery-timeout" : "sleep-inactive-ac-timeout"); + timeout_sleep = CLAMP (timeout_sleep_, 0, G_MAXINT); + } + + clear_idle_watch (manager->idle_monitor, + &manager->idle_sleep_id); + clear_idle_watch (manager->idle_monitor, + &manager->idle_sleep_warning_id); + + /* don't do any power saving if we're a VM */ + if (manager->is_virtual_machine && + (action_type == GSD_POWER_ACTION_SUSPEND || + action_type == GSD_POWER_ACTION_HIBERNATE)) { + g_debug ("Ignoring sleep timeout with suspend action inside VM"); + timeout_sleep = 0; + } + + /* don't do any automatic logout if we are in GDM */ + if (g_getenv ("RUNNING_UNDER_GDM") && + (action_type == GSD_POWER_ACTION_LOGOUT)) { + g_debug ("Ignoring sleep timeout with logout action inside GDM"); + timeout_sleep = 0; + } + + if (timeout_sleep != 0) { + g_debug ("setting up sleep callback %is", timeout_sleep); + + if (action_type != GSD_POWER_ACTION_NOTHING) { + manager->idle_sleep_id = gnome_idle_monitor_add_idle_watch (manager->idle_monitor, + timeout_sleep * 1000, + idle_triggered_idle_cb, manager, NULL); + } + + if (action_type == GSD_POWER_ACTION_LOGOUT || + action_type == GSD_POWER_ACTION_SUSPEND || + action_type == GSD_POWER_ACTION_HIBERNATE) { + guint timeout_sleep_warning_msec; + + manager->sleep_action_type = action_type; + timeout_sleep_warning_msec = timeout_sleep * IDLE_DELAY_TO_IDLE_DIM_MULTIPLIER * 1000; + if (timeout_sleep_warning_msec * 1000 < MINIMUM_IDLE_DIM_DELAY) { + /* 0 is not a valid idle timeout */ + timeout_sleep_warning_msec = 1; + } + + g_debug ("setting up sleep warning callback %i msec", timeout_sleep_warning_msec); + + manager->idle_sleep_warning_id = gnome_idle_monitor_add_idle_watch (manager->idle_monitor, + timeout_sleep_warning_msec, + idle_triggered_idle_cb, manager, NULL); + } + } + + if (manager->idle_sleep_warning_id == 0) + notify_close_if_showing (&manager->notification_sleep_warning); + + /* set up dim callback for when the screen lock is not active, + * but only if we actually want to dim. */ + timeout_dim = 0; + if (manager->screensaver_active) { + /* Don't dim when the screen lock is active */ + } else if (!on_battery) { + /* Don't dim when charging */ + } else if (manager->battery_is_low) { + /* Aggressively blank when battery is low */ + timeout_dim = SCREENSAVER_TIMEOUT_BLANK; + } else { + if (g_settings_get_boolean (manager->settings, "idle-dim")) { + timeout_dim = g_settings_get_uint (manager->settings_bus, + "idle-delay"); + if (timeout_dim == 0) { + timeout_dim = IDLE_DIM_BLANK_DISABLED_MIN; + } else { + timeout_dim *= IDLE_DELAY_TO_IDLE_DIM_MULTIPLIER; + /* Don't bother dimming if the idle-delay is + * too low, we'll do that when we bring down the + * screen lock */ + if (timeout_dim < MINIMUM_IDLE_DIM_DELAY) + timeout_dim = 0; + } + } + } + + clear_idle_watch (manager->idle_monitor, + &manager->idle_dim_id); + + if (timeout_dim != 0) { + g_debug ("setting up dim callback for %is", timeout_dim); + + manager->idle_dim_id = gnome_idle_monitor_add_idle_watch (manager->idle_monitor, + timeout_dim * 1000, + idle_triggered_idle_cb, manager, NULL); + } +} + +static void +main_battery_or_ups_low_changed (GsdPowerManager *manager, + gboolean is_low) +{ + if (is_low == manager->battery_is_low) + return; + manager->battery_is_low = is_low; + idle_configure (manager); +} + +static gboolean +temporary_unidle_done_cb (GsdPowerManager *manager) +{ + idle_set_mode (manager, manager->previous_idle_mode); + manager->temporary_unidle_on_ac_id = 0; + return FALSE; +} + +static void +set_temporary_unidle_on_ac (GsdPowerManager *manager, + gboolean enable) +{ + if (!enable) { + /* Don't automatically go back to the previous idle + mode. The caller probably has a better idea of + which state to move to when disabling us. */ + if (manager->temporary_unidle_on_ac_id != 0) { + g_source_remove (manager->temporary_unidle_on_ac_id); + manager->temporary_unidle_on_ac_id = 0; + } + } else { + /* Don't overwrite the previous idle mode when an unidle is + * already on-going */ + if (manager->temporary_unidle_on_ac_id != 0) { + g_source_remove (manager->temporary_unidle_on_ac_id); + } else { + manager->previous_idle_mode = manager->current_idle_mode; + idle_set_mode (manager, GSD_POWER_IDLE_MODE_NORMAL); + } + manager->temporary_unidle_on_ac_id = g_timeout_add_seconds (POWER_UP_TIME_ON_AC, + (GSourceFunc) temporary_unidle_done_cb, + manager); + g_source_set_name_by_id (manager->temporary_unidle_on_ac_id, "[gnome-settings-daemon] temporary_unidle_done_cb"); + } +} + +static void +up_client_on_battery_cb (UpClient *client, + GParamSpec *pspec, + GsdPowerManager *manager) +{ + if (up_client_get_on_battery (manager->up_client)) { + ca_context_play (ca_gtk_context_get (), 0, + CA_PROP_EVENT_ID, "power-unplug", + /* TRANSLATORS: this is the sound description */ + CA_PROP_EVENT_DESCRIPTION, _("On battery power"), NULL); + } else { + ca_context_play (ca_gtk_context_get (), 0, + CA_PROP_EVENT_ID, "power-plug", + /* TRANSLATORS: this is the sound description */ + CA_PROP_EVENT_DESCRIPTION, _("On AC power"), NULL); + + } + + idle_configure (manager); + + if (manager->lid_is_closed) + return; + + if (manager->current_idle_mode == GSD_POWER_IDLE_MODE_BLANK || + manager->current_idle_mode == GSD_POWER_IDLE_MODE_DIM || + manager->temporary_unidle_on_ac_id != 0) + set_temporary_unidle_on_ac (manager, TRUE); +} + +static void +gsd_power_manager_finalize (GObject *object) +{ + GsdPowerManager *manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_POWER_MANAGER (object)); + + manager = GSD_POWER_MANAGER (object); + + g_return_if_fail (manager != NULL); + + gsd_power_manager_stop (manager); + + g_clear_object (&manager->connection); + + if (manager->name_id != 0) + g_bus_unown_name (manager->name_id); + + if (manager->iio_proxy_watch_id != 0) + g_bus_unwatch_name (manager->iio_proxy_watch_id); + manager->iio_proxy_watch_id = 0; + + G_OBJECT_CLASS (gsd_power_manager_parent_class)->finalize (object); +} + +static void +gsd_power_manager_class_init (GsdPowerManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_power_manager_finalize; + + notify_init ("gnome-settings-daemon"); +} + +static void +handle_screensaver_active (GsdPowerManager *manager, + GVariant *parameters) +{ + gboolean active; + + g_variant_get (parameters, "(b)", &active); + g_debug ("Received screensaver ActiveChanged signal: %d (old: %d)", active, manager->screensaver_active); + if (manager->screensaver_active != active) { + manager->screensaver_active = active; + idle_configure (manager); + + /* Setup blank as soon as the screensaver comes on, + * and its fade has finished. + * + * See also idle_configure() */ + if (active) + idle_set_mode (manager, GSD_POWER_IDLE_MODE_BLANK); + } +} + +static void +handle_wake_up_screen (GsdPowerManager *manager) +{ + set_temporary_unidle_on_ac (manager, TRUE); +} + +static void +screensaver_signal_cb (GDBusProxy *proxy, + const gchar *sender_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + if (g_strcmp0 (signal_name, "ActiveChanged") == 0) + handle_screensaver_active (GSD_POWER_MANAGER (user_data), parameters); + else if (g_strcmp0 (signal_name, "WakeUpScreen") == 0) + handle_wake_up_screen (GSD_POWER_MANAGER (user_data)); +} + +static void +power_keyboard_proxy_ready_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GVariant *k_now = NULL; + GVariant *k_max = NULL; + GError *error = NULL; + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + gint percentage; + + manager->upower_kbd_proxy = g_dbus_proxy_new_for_bus_finish (res, &error); + if (manager->upower_kbd_proxy == NULL) { + g_warning ("Could not connect to UPower: %s", + error->message); + g_error_free (error); + goto out; + } + + g_signal_connect (manager->upower_kbd_proxy, "g-signal", + G_CALLBACK (upower_kbd_proxy_signal_cb), + manager); + + k_now = g_dbus_proxy_call_sync (manager->upower_kbd_proxy, + "GetBrightness", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + &error); + if (k_now == NULL) { + if (error->domain != G_DBUS_ERROR || + error->code != G_DBUS_ERROR_UNKNOWN_METHOD) { + g_warning ("Failed to get brightness: %s", + error->message); + } else { + /* Keyboard brightness is not available */ + g_clear_object (&manager->upower_kbd_proxy); + } + g_error_free (error); + goto out; + } + + k_max = g_dbus_proxy_call_sync (manager->upower_kbd_proxy, + "GetMaxBrightness", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + &error); + if (k_max == NULL) { + g_warning ("Failed to get max brightness: %s", error->message); + g_error_free (error); + goto out; + } + + g_variant_get (k_now, "(i)", &manager->kbd_brightness_now); + g_variant_get (k_max, "(i)", &manager->kbd_brightness_max); + + /* set brightness to max if not currently set so is something + * sensible */ + if (manager->kbd_brightness_now < 0) { + gboolean ret; + ret = upower_kbd_set_brightness (manager, + manager->kbd_brightness_max, + &error); + if (!ret) { + g_warning ("failed to initialize kbd backlight to %i: %s", + manager->kbd_brightness_max, + error->message); + g_error_free (error); + } + } + + /* Tell the front-end that the brightness changed from + * its default "-1/no keyboard backlight available" default */ + percentage = ABS_TO_PERCENTAGE (0, + manager->kbd_brightness_max, + manager->kbd_brightness_now); + backlight_iface_emit_changed (manager, GSD_POWER_DBUS_INTERFACE_KEYBOARD, percentage, "initial value"); + +out: + if (k_now != NULL) + g_variant_unref (k_now); + if (k_max != NULL) + g_variant_unref (k_max); +} + +static void +show_sleep_warning (GsdPowerManager *manager) +{ + /* close any existing notification of this class */ + notify_close_if_showing (&manager->notification_sleep_warning); + + /* create a new notification */ + switch (manager->sleep_action_type) { + case GSD_POWER_ACTION_LOGOUT: + create_notification (_("Automatic logout"), _("You will soon log out because of inactivity."), + NULL, NOTIFICATION_PRIVACY_USER, + &manager->notification_sleep_warning); + break; + case GSD_POWER_ACTION_SUSPEND: + create_notification (_("Automatic suspend"), _("Computer will suspend very soon because of inactivity."), + NULL, NOTIFICATION_PRIVACY_SYSTEM, + &manager->notification_sleep_warning); + break; + case GSD_POWER_ACTION_HIBERNATE: + create_notification (_("Automatic hibernation"), _("Computer will suspend very soon because of inactivity."), + NULL, NOTIFICATION_PRIVACY_SYSTEM, + &manager->notification_sleep_warning); + break; + default: + g_assert_not_reached (); + break; + } + notify_notification_set_timeout (manager->notification_sleep_warning, + NOTIFY_EXPIRES_NEVER); + notify_notification_set_urgency (manager->notification_sleep_warning, + NOTIFY_URGENCY_CRITICAL); + + notify_notification_show (manager->notification_sleep_warning, NULL); +} + +static void +idle_set_mode_no_temp (GsdPowerManager *manager, + GsdPowerIdleMode mode) +{ + if (manager->temporary_unidle_on_ac_id != 0) { + manager->previous_idle_mode = mode; + return; + } + + idle_set_mode (manager, mode); +} + +static void +idle_triggered_idle_cb (GnomeIdleMonitor *monitor, + guint watch_id, + gpointer user_data) +{ + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + const char *id_name; + + id_name = idle_watch_id_to_string (manager, watch_id); + if (id_name == NULL) + g_debug ("idletime watch: %i", watch_id); + else + g_debug ("idletime watch: %s (%i)", id_name, watch_id); + + if (watch_id == manager->idle_dim_id) { + idle_set_mode_no_temp (manager, GSD_POWER_IDLE_MODE_DIM); + } else if (watch_id == manager->idle_blank_id) { + idle_set_mode_no_temp (manager, GSD_POWER_IDLE_MODE_BLANK); + } else if (watch_id == manager->idle_sleep_id) { + idle_set_mode_no_temp (manager, GSD_POWER_IDLE_MODE_SLEEP); + } else if (watch_id == manager->idle_sleep_warning_id) { + show_sleep_warning (manager); + + if (manager->user_active_id < 1) { + manager->user_active_id = gnome_idle_monitor_add_user_active_watch (manager->idle_monitor, + idle_became_active_cb, + manager, + NULL); + g_debug ("installing idle_became_active_cb to clear sleep warning on activity (%i)", + manager->user_active_id); + } + } +} + +static void +idle_became_active_cb (GnomeIdleMonitor *monitor, + guint watch_id, + gpointer user_data) +{ + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + + g_debug ("idletime reset (%i)", watch_id); + + set_temporary_unidle_on_ac (manager, FALSE); + + /* close any existing notification about idleness */ + notify_close_if_showing (&manager->notification_sleep_warning); + + idle_set_mode (manager, GSD_POWER_IDLE_MODE_NORMAL); + manager->user_active_id = 0; +} + +static void +ch_backlight_renormalize (GsdPowerManager *manager) +{ + if (manager->ambient_percentage_old < 0) + return; + if (manager->ambient_last_absolute < 0) + return; + manager->ambient_norm_value = manager->ambient_last_absolute / + (gdouble) manager->ambient_percentage_old; + manager->ambient_norm_value *= 100.f; + manager->ambient_norm_required = FALSE; +} + +static void +engine_settings_key_changed_cb (GSettings *settings, + const gchar *key, + GsdPowerManager *manager) +{ + if (g_str_has_prefix (key, "sleep-inactive") || + g_str_equal (key, "idle-delay") || + g_str_equal (key, "idle-dim")) { + idle_configure (manager); + return; + } +} + +static void +engine_session_properties_changed_cb (GDBusProxy *session, + GVariant *changed, + char **invalidated, + GsdPowerManager *manager) +{ + GVariant *v; + + v = g_variant_lookup_value (changed, "SessionIsActive", G_VARIANT_TYPE_BOOLEAN); + if (v) { + gboolean active; + + active = g_variant_get_boolean (v); + g_debug ("Received session is active change: now %s", active ? "active" : "inactive"); + manager->session_is_active = active; + /* when doing the fast-user-switch into a new account, + * ensure the new account is undimmed and with the backlight on */ + if (active) { + idle_set_mode (manager, GSD_POWER_IDLE_MODE_NORMAL); + iio_proxy_claim_light (manager, TRUE); + } else { + iio_proxy_claim_light (manager, FALSE); + } + g_variant_unref (v); + + sync_lid_inhibitor (manager); + } + + v = g_variant_lookup_value (changed, "InhibitedActions", G_VARIANT_TYPE_UINT32); + if (v) { + g_variant_unref (v); + g_debug ("Received gnome session inhibitor change"); + idle_configure (manager); + } +} + +static void +inhibit_lid_switch_done (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GDBusProxy *proxy = G_DBUS_PROXY (source); + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + GError *error = NULL; + GVariant *res; + GUnixFDList *fd_list = NULL; + gint idx; + + res = g_dbus_proxy_call_with_unix_fd_list_finish (proxy, &fd_list, result, &error); + if (res == NULL) { + g_warning ("Unable to inhibit lid switch: %s", error->message); + g_error_free (error); + } else { + g_variant_get (res, "(h)", &idx); + manager->inhibit_lid_switch_fd = g_unix_fd_list_get (fd_list, idx, &error); + if (manager->inhibit_lid_switch_fd == -1) { + g_warning ("Failed to receive system inhibitor fd: %s", error->message); + g_error_free (error); + } + g_debug ("System inhibitor fd is %d", manager->inhibit_lid_switch_fd); + g_object_unref (fd_list); + g_variant_unref (res); + } +} + +static void +inhibit_lid_switch (GsdPowerManager *manager) +{ + GVariant *params; + + if (manager->inhibit_lid_switch_taken) { + g_debug ("already inhibited lid-switch"); + return; + } + g_debug ("Adding lid switch system inhibitor"); + manager->inhibit_lid_switch_taken = TRUE; + + params = g_variant_new ("(ssss)", + "handle-lid-switch", + g_get_user_name (), + "External monitor attached or configuration changed recently", + "block"); + g_dbus_proxy_call_with_unix_fd_list (manager->logind_proxy, + "Inhibit", + params, + 0, + G_MAXINT, + NULL, + NULL, + inhibit_lid_switch_done, + manager); +} + +static void +uninhibit_lid_switch (GsdPowerManager *manager) +{ + if (manager->inhibit_lid_switch_fd == -1) { + g_debug ("no lid-switch inhibitor"); + return; + } + g_debug ("Removing lid switch system inhibitor"); + close (manager->inhibit_lid_switch_fd); + manager->inhibit_lid_switch_fd = -1; + manager->inhibit_lid_switch_taken = FALSE; +} + +static void +inhibit_suspend_done (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GDBusProxy *proxy = G_DBUS_PROXY (source); + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + GError *error = NULL; + GVariant *res; + GUnixFDList *fd_list = NULL; + gint idx; + + res = g_dbus_proxy_call_with_unix_fd_list_finish (proxy, &fd_list, result, &error); + if (res == NULL) { + g_warning ("Unable to inhibit suspend: %s", error->message); + g_error_free (error); + } else { + g_variant_get (res, "(h)", &idx); + manager->inhibit_suspend_fd = g_unix_fd_list_get (fd_list, idx, &error); + if (manager->inhibit_suspend_fd == -1) { + g_warning ("Failed to receive system inhibitor fd: %s", error->message); + g_error_free (error); + } + g_debug ("System inhibitor fd is %d", manager->inhibit_suspend_fd); + g_object_unref (fd_list); + g_variant_unref (res); + } +} + +/* We take a delay inhibitor here, which causes logind to send a + * PrepareForSleep signal, which gives us a chance to lock the screen + * and do some other preparations. + */ +static void +inhibit_suspend (GsdPowerManager *manager) +{ + if (manager->inhibit_suspend_taken) { + g_debug ("already inhibited lid-switch"); + return; + } + g_debug ("Adding suspend delay inhibitor"); + manager->inhibit_suspend_taken = TRUE; + g_dbus_proxy_call_with_unix_fd_list (manager->logind_proxy, + "Inhibit", + g_variant_new ("(ssss)", + "sleep", + g_get_user_name (), + "GNOME needs to lock the screen", + "delay"), + 0, + G_MAXINT, + NULL, + NULL, + inhibit_suspend_done, + manager); +} + +static void +uninhibit_suspend (GsdPowerManager *manager) +{ + if (manager->inhibit_suspend_fd == -1) { + g_debug ("no suspend delay inhibitor"); + return; + } + g_debug ("Removing suspend delay inhibitor"); + close (manager->inhibit_suspend_fd); + manager->inhibit_suspend_fd = -1; + manager->inhibit_suspend_taken = FALSE; +} + +static void +sync_lid_inhibitor (GsdPowerManager *manager) +{ + g_debug ("Syncing lid inhibitor and grabbing it temporarily"); + + /* Uninhibiting is done in inhibit_lid_switch_timer_cb, + * since we want to give users a few seconds when unplugging + * and replugging an external monitor, not suspend right away. + */ + inhibit_lid_switch (manager); + restart_inhibit_lid_switch_timer (manager); +} + +static void +on_randr_event (GnomeRRScreen *screen, gpointer user_data) +{ + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + + g_debug ("Screen configuration changed"); + + sync_lid_inhibitor (manager); +} + +static void +handle_suspend_actions (GsdPowerManager *manager) +{ + backlight_disable (manager); + uninhibit_suspend (manager); +} + +static void +handle_resume_actions (GsdPowerManager *manager) +{ + /* ensure we turn the panel back on after resume */ + backlight_enable (manager); + + /* set up the delay again */ + inhibit_suspend (manager); +} + +static void +logind_proxy_signal_cb (GDBusProxy *proxy, + const gchar *sender_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + gboolean is_about_to_suspend; + + if (g_strcmp0 (signal_name, "PrepareForSleep") != 0) + return; + g_variant_get (parameters, "(b)", &is_about_to_suspend); + if (is_about_to_suspend) { + handle_suspend_actions (manager); + } else { + handle_resume_actions (manager); + } +} + +static void +on_rr_screen_acquired (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GsdPowerManager *manager = user_data; + GError *error = NULL; + + gnome_settings_profile_start (NULL); + + manager->rr_screen = gnome_rr_screen_new_finish (result, &error); + + if (error) { + g_warning ("Could not create GnomeRRScreen: %s\n", error->message); + g_error_free (error); + gnome_settings_profile_end (NULL); + + return; + } + + /* Resolve screen backlight */ + manager->backlight = gsd_backlight_new (manager->rr_screen, NULL); + + if (manager->backlight) + g_signal_connect_object (manager->backlight, + "notify::brightness", + G_CALLBACK (backlight_notify_brightness_cb), + manager, G_CONNECT_SWAPPED); + + /* Set up a delay inhibitor to be informed about suspend attempts */ + g_signal_connect (manager->logind_proxy, "g-signal", + G_CALLBACK (logind_proxy_signal_cb), + manager); + inhibit_suspend (manager); + + /* track the active session */ + manager->session = gnome_settings_bus_get_session_proxy (); + g_signal_connect_object (manager->session, "g-properties-changed", + G_CALLBACK (engine_session_properties_changed_cb), + manager, 0); + manager->session_is_active = is_session_active (manager); + + /* set up the screens */ + if (manager->lid_is_present) { + g_signal_connect (manager->rr_screen, "changed", G_CALLBACK (on_randr_event), manager); + watch_external_monitor (manager->rr_screen); + on_randr_event (manager->rr_screen, manager); + } + + manager->screensaver_proxy = gnome_settings_bus_get_screen_saver_proxy (); + + g_signal_connect (manager->screensaver_proxy, "g-signal", + G_CALLBACK (screensaver_signal_cb), manager); + + manager->kbd_brightness_old = -1; + manager->kbd_brightness_pre_dim = -1; + manager->pre_dim_brightness = -1; + g_signal_connect (manager->settings, "changed", + G_CALLBACK (engine_settings_key_changed_cb), manager); + g_signal_connect (manager->settings_bus, "changed", + G_CALLBACK (engine_settings_key_changed_cb), manager); + g_signal_connect (manager->up_client, "device-added", + G_CALLBACK (engine_device_added_cb), manager); + g_signal_connect (manager->up_client, "device-removed", + G_CALLBACK (engine_device_removed_cb), manager); + g_signal_connect_after (manager->up_client, "notify::lid-is-closed", + G_CALLBACK (lid_state_changed_cb), manager); + g_signal_connect (manager->up_client, "notify::on-battery", + G_CALLBACK (up_client_on_battery_cb), manager); + + /* connect to UPower for keyboard backlight control */ + manager->kbd_brightness_now = -1; + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES, + NULL, + UPOWER_DBUS_NAME, + UPOWER_DBUS_PATH_KBDBACKLIGHT, + UPOWER_DBUS_INTERFACE_KBDBACKLIGHT, + NULL, + power_keyboard_proxy_ready_cb, + manager); + + manager->devices_array = g_ptr_array_new_with_free_func (g_object_unref); + manager->devices_notified_ht = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + + /* create a fake virtual composite battery */ + manager->device_composite = up_client_get_display_device (manager->up_client); + g_signal_connect (manager->device_composite, "notify::warning-level", + G_CALLBACK (engine_device_warning_changed_cb), manager); + + /* create IDLETIME watcher */ + manager->idle_monitor = gnome_idle_monitor_new (); + + /* coldplug the engine */ + engine_coldplug (manager); + idle_configure (manager); + + /* ensure the default dpms timeouts are cleared */ + backlight_enable (manager); + + if (!gnome_settings_is_wayland ()) + manager->xscreensaver_watchdog_timer_id = gsd_power_enable_screensaver_watchdog (); + + /* don't blank inside a VM */ + manager->is_virtual_machine = gsd_power_is_hardware_a_vm (); + + /* queue a signal in case the proxy from gnome-shell was created before we got here + (likely, considering that to get here we need a reply from gnome-shell) + */ + if (manager->backlight) { + manager->ambient_percentage_old = gsd_backlight_get_brightness (manager->backlight, NULL); + backlight_iface_emit_changed (manager, GSD_POWER_DBUS_INTERFACE_SCREEN, + manager->ambient_percentage_old, NULL); + } else { + backlight_iface_emit_changed (manager, GSD_POWER_DBUS_INTERFACE_SCREEN, -1, NULL); + } + + gnome_settings_profile_end (NULL); +} + +static void +iio_proxy_changed (GsdPowerManager *manager) +{ + GVariant *val_has = NULL; + GVariant *val_als = NULL; + gdouble brightness; + gdouble alpha; + gint64 current_time; + gint pc; + + /* no display hardware */ + if (!manager->backlight) + return; + + /* disabled */ + if (!g_settings_get_boolean (manager->settings, "ambient-enabled")) + return; + + /* get latest results, which do not have to be Lux */ + val_has = g_dbus_proxy_get_cached_property (manager->iio_proxy, "HasAmbientLight"); + if (val_has == NULL || !g_variant_get_boolean (val_has)) + goto out; + val_als = g_dbus_proxy_get_cached_property (manager->iio_proxy, "LightLevel"); + if (val_als == NULL || g_variant_get_double (val_als) == 0.0) + goto out; + manager->ambient_last_absolute = g_variant_get_double (val_als); + g_debug ("Read last absolute light level: %f", manager->ambient_last_absolute); + + /* the user has asked to renormalize */ + if (manager->ambient_norm_required) { + g_debug ("Renormalizing light level from old light percentage: %.1f%%", + manager->ambient_percentage_old); + manager->ambient_accumulator = manager->ambient_percentage_old; + ch_backlight_renormalize (manager); + } + + /* time-weighted constant for moving average */ + current_time = g_get_monotonic_time(); + if (manager->ambient_last_time) + alpha = 1.0f / (1.0f + (GSD_AMBIENT_TIME_CONSTANT / (current_time - manager->ambient_last_time))); + else + alpha = 0.0f; + manager->ambient_last_time = current_time; + + /* calculate exponential weighted moving average */ + brightness = manager->ambient_last_absolute * 100.f / manager->ambient_norm_value; + brightness = MIN (brightness, 100.f); + brightness = MAX (brightness, 0.f); + + manager->ambient_accumulator = (alpha * brightness) + + (1.0 - alpha) * manager->ambient_accumulator; + + /* no valid readings yet */ + if (manager->ambient_accumulator < 0.f) + goto out; + + /* set new value */ + g_debug ("Setting brightness from ambient %.1f%%", + manager->ambient_accumulator); + pc = manager->ambient_accumulator; + + if (manager->backlight) + gsd_backlight_set_brightness_async (manager->backlight, pc, NULL, NULL, NULL); + + /* Assume setting worked. */ + manager->ambient_percentage_old = pc; +out: + g_clear_pointer (&val_has, g_variant_unref); + g_clear_pointer (&val_als, g_variant_unref); +} + +static void +iio_proxy_changed_cb (GDBusProxy *proxy, + GVariant *changed_properties, + GStrv invalidated_properties, + gpointer user_data) +{ + iio_proxy_changed ((GsdPowerManager *) user_data); +} + +static void +iio_proxy_appeared_cb (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer user_data) +{ + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + manager->iio_proxy = + g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + 0, + NULL, + "net.hadess.SensorProxy", + "/net/hadess/SensorProxy", + "net.hadess.SensorProxy", + NULL, + NULL); + iio_proxy_claim_light (manager, TRUE); +} + +static void +iio_proxy_vanished_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + g_clear_object (&manager->iio_proxy); +} + +gboolean +gsd_power_manager_start (GsdPowerManager *manager, + GError **error) +{ + g_debug ("Starting power manager"); + gnome_settings_profile_start (NULL); + + /* Check whether we have a lid first */ + manager->up_client = up_client_new (); + manager->lid_is_present = up_client_get_lid_is_present (manager->up_client); + if (manager->lid_is_present) + manager->lid_is_closed = up_client_get_lid_is_closed (manager->up_client); + + /* Set up the logind proxy */ + manager->logind_proxy = + g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + 0, + NULL, + SYSTEMD_DBUS_NAME, + SYSTEMD_DBUS_PATH, + SYSTEMD_DBUS_INTERFACE, + NULL, + error); + if (manager->logind_proxy == NULL) { + g_debug ("No systemd (logind) support, disabling plugin"); + return FALSE; + } + + /* coldplug the list of screens */ + gnome_rr_screen_new_async (gdk_screen_get_default (), + on_rr_screen_acquired, manager); + + manager->settings = g_settings_new (GSD_POWER_SETTINGS_SCHEMA); + manager->settings_screensaver = g_settings_new ("org.gnome.desktop.screensaver"); + manager->settings_bus = g_settings_new ("org.gnome.desktop.session"); + + /* setup ambient light support */ + manager->iio_proxy_watch_id = + g_bus_watch_name (G_BUS_TYPE_SYSTEM, + "net.hadess.SensorProxy", + G_BUS_NAME_WATCHER_FLAGS_NONE, + iio_proxy_appeared_cb, + iio_proxy_vanished_cb, + manager, NULL); + manager->ambient_norm_required = TRUE; + manager->ambient_accumulator = -1.f; + manager->ambient_norm_value = -1.f; + manager->ambient_percentage_old = -1.f; + manager->ambient_last_absolute = -1.f; + manager->ambient_last_time = 0; + + gnome_settings_profile_end (NULL); + return TRUE; +} + +void +gsd_power_manager_stop (GsdPowerManager *manager) +{ + g_debug ("Stopping power manager"); + + if (manager->inhibit_lid_switch_timer_id != 0) { + g_source_remove (manager->inhibit_lid_switch_timer_id); + manager->inhibit_lid_switch_timer_id = 0; + } + + if (manager->cancellable != NULL) { + g_cancellable_cancel (manager->cancellable); + g_clear_object (&manager->cancellable); + } + + g_clear_pointer (&manager->introspection_data, g_dbus_node_info_unref); + + if (manager->up_client) + g_signal_handlers_disconnect_by_data (manager->up_client, manager); + + g_clear_object (&manager->session); + g_clear_object (&manager->settings); + g_clear_object (&manager->settings_screensaver); + g_clear_object (&manager->settings_bus); + g_clear_object (&manager->up_client); + + iio_proxy_claim_light (manager, FALSE); + g_clear_object (&manager->iio_proxy); + + if (manager->inhibit_lid_switch_fd != -1) { + close (manager->inhibit_lid_switch_fd); + manager->inhibit_lid_switch_fd = -1; + manager->inhibit_lid_switch_taken = FALSE; + } + if (manager->inhibit_suspend_fd != -1) { + close (manager->inhibit_suspend_fd); + manager->inhibit_suspend_fd = -1; + manager->inhibit_suspend_taken = FALSE; + } + + g_clear_object (&manager->logind_proxy); + g_clear_object (&manager->rr_screen); + + g_clear_pointer (&manager->devices_array, g_ptr_array_unref); + g_clear_object (&manager->device_composite); + g_clear_pointer (&manager->devices_notified_ht, g_hash_table_destroy); + + g_clear_object (&manager->screensaver_proxy); + + play_loop_stop (&manager->critical_alert_timeout_id); + + g_clear_object (&manager->idle_monitor); + g_clear_object (&manager->upower_kbd_proxy); + + if (manager->xscreensaver_watchdog_timer_id > 0) { + g_source_remove (manager->xscreensaver_watchdog_timer_id); + manager->xscreensaver_watchdog_timer_id = 0; + } +} + +static void +gsd_power_manager_init (GsdPowerManager *manager) +{ + manager->inhibit_lid_switch_fd = -1; + manager->inhibit_suspend_fd = -1; + manager->cancellable = g_cancellable_new (); +} + +/* returns new level */ +static void +handle_method_call_keyboard (GsdPowerManager *manager, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + gint step; + gint value = -1; + gboolean ret; + guint percentage; + GError *error = NULL; + + if (g_strcmp0 (method_name, "StepUp") == 0) { + g_debug ("keyboard step up"); + step = BRIGHTNESS_STEP_AMOUNT (manager->kbd_brightness_max); + value = MIN (manager->kbd_brightness_now + step, + manager->kbd_brightness_max); + ret = upower_kbd_set_brightness (manager, value, &error); + + } else if (g_strcmp0 (method_name, "StepDown") == 0) { + g_debug ("keyboard step down"); + step = BRIGHTNESS_STEP_AMOUNT (manager->kbd_brightness_max); + value = MAX (manager->kbd_brightness_now - step, 0); + ret = upower_kbd_set_brightness (manager, value, &error); + + } else if (g_strcmp0 (method_name, "Toggle") == 0) { + value = upower_kbd_toggle (manager, &error); + ret = (value >= 0); + + } else { + g_assert_not_reached (); + } + + /* return value */ + if (!ret) { + g_dbus_method_invocation_take_error (invocation, + error); + backlight_iface_emit_changed (manager, GSD_POWER_DBUS_INTERFACE_KEYBOARD, -1, method_name); + } else { + percentage = ABS_TO_PERCENTAGE (0, + manager->kbd_brightness_max, + value); + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(i)", + percentage)); + backlight_iface_emit_changed (manager, GSD_POWER_DBUS_INTERFACE_KEYBOARD, percentage, method_name); + } +} + +static void +backlight_brightness_step_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + GsdBacklight *backlight = GSD_BACKLIGHT (object); + GDBusMethodInvocation *invocation = G_DBUS_METHOD_INVOCATION (user_data); + GsdPowerManager *manager; + GError *error = NULL; + const char *connector; + gint brightness; + + manager = g_object_get_data (G_OBJECT (invocation), "gsd-power-manager"); + brightness = gsd_backlight_set_brightness_finish (backlight, res, &error); + + /* ambient brightness no longer valid */ + manager->ambient_percentage_old = brightness; + manager->ambient_norm_required = TRUE; + + if (error) { + g_dbus_method_invocation_take_error (invocation, + error); + } else { + connector = gsd_backlight_get_connector (backlight); + + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(is)", + brightness, + connector ? connector : "")); + } +} + +/* Callback */ +static void +backlight_brightness_set_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + GsdBacklight *backlight = GSD_BACKLIGHT (object); + gint brightness; + + /* Return the invocation. */ + brightness = gsd_backlight_set_brightness_finish (backlight, res, NULL); + + if (brightness >= 0) { + manager->ambient_percentage_old = brightness; + manager->ambient_norm_required = TRUE; + } + + g_object_unref (manager); +} + +static void +handle_method_call_screen (GsdPowerManager *manager, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + if (!manager->backlight) { + g_dbus_method_invocation_return_error_literal (invocation, + GSD_POWER_MANAGER_ERROR, GSD_POWER_MANAGER_ERROR_NO_BACKLIGHT, + "No usable backlight could be found!"); + return; + } + + g_object_set_data_full (G_OBJECT (invocation), "gsd-power-manager", g_object_ref (manager), g_object_unref); + + if (g_strcmp0 (method_name, "StepUp") == 0) { + g_debug ("screen step up"); + gsd_backlight_step_up_async (manager->backlight, NULL, backlight_brightness_step_cb, invocation); + + } else if (g_strcmp0 (method_name, "StepDown") == 0) { + g_debug ("screen step down"); + gsd_backlight_step_down_async (manager->backlight, NULL, backlight_brightness_step_cb, invocation); + + } else if (g_strcmp0 (method_name, "Cycle") == 0) { + g_debug ("screen cycle up"); + gsd_backlight_cycle_up_async (manager->backlight, NULL, backlight_brightness_step_cb, invocation); + + } else { + g_assert_not_reached (); + } +} + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + + /* Check session pointer as a proxy for whether the manager is in the + start or stop state */ + if (manager->session == NULL) { + return; + } + + g_debug ("Calling method '%s.%s' for Power", + interface_name, method_name); + + if (g_strcmp0 (interface_name, GSD_POWER_DBUS_INTERFACE_SCREEN) == 0) { + handle_method_call_screen (manager, + method_name, + parameters, + invocation); + } else if (g_strcmp0 (interface_name, GSD_POWER_DBUS_INTERFACE_KEYBOARD) == 0) { + handle_method_call_keyboard (manager, + method_name, + parameters, + invocation); + } else { + g_warning ("not recognised interface: %s", interface_name); + } +} + +static GVariant * +handle_get_property_other (GsdPowerManager *manager, + const gchar *interface_name, + const gchar *property_name, + GError **error) +{ + GVariant *retval = NULL; + gint32 value; + + if (g_strcmp0 (property_name, "Brightness") != 0) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "No such property: %s", property_name); + return NULL; + } + + if (g_strcmp0 (interface_name, GSD_POWER_DBUS_INTERFACE_SCREEN) == 0) { + if (manager->backlight) + value = gsd_backlight_get_brightness (manager->backlight, NULL); + else + value = -1; + + retval = g_variant_new_int32 (value); + } else if (manager->upower_kbd_proxy && + g_strcmp0 (interface_name, GSD_POWER_DBUS_INTERFACE_KEYBOARD) == 0) { + value = ABS_TO_PERCENTAGE (0, + manager->kbd_brightness_max, + manager->kbd_brightness_now); + retval = g_variant_new_int32 (value); + } + + if (retval == NULL) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Failed to get property %s on interface %s", + property_name, interface_name); + } + return retval; +} + +static GVariant * +handle_get_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, gpointer user_data) +{ + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + + /* Check session pointer as a proxy for whether the manager is in the + start or stop state */ + if (manager->session == NULL) { + g_set_error_literal (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "No session"); + return NULL; + } + + if (g_strcmp0 (interface_name, GSD_POWER_DBUS_INTERFACE_SCREEN) == 0 || + g_strcmp0 (interface_name, GSD_POWER_DBUS_INTERFACE_KEYBOARD) == 0) { + return handle_get_property_other (manager, interface_name, property_name, error); + } else { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "No such interface: %s", interface_name); + return NULL; + } +} + +static gboolean +handle_set_property_other (GsdPowerManager *manager, + const gchar *interface_name, + const gchar *property_name, + GVariant *value, + GError **error) +{ + gint32 brightness_value; + + if (g_strcmp0 (property_name, "Brightness") != 0) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "No such property: %s", property_name); + return FALSE; + } + + if (g_strcmp0 (interface_name, GSD_POWER_DBUS_INTERFACE_SCREEN) == 0) { + /* To do error reporting we would need to handle the Set call + * instead of doing it through set_property. + * But none of our DBus API users actually read the result. */ + g_variant_get (value, "i", &brightness_value); + if (manager->backlight) { + gsd_backlight_set_brightness_async (manager->backlight, brightness_value, + NULL, + backlight_brightness_set_cb, g_object_ref (manager)); + return TRUE; + } else { + g_set_error_literal (error, GSD_POWER_MANAGER_ERROR, GSD_POWER_MANAGER_ERROR_NO_BACKLIGHT, + "No usable backlight could be found!"); + return FALSE; + } + + } else if (g_strcmp0 (interface_name, GSD_POWER_DBUS_INTERFACE_KEYBOARD) == 0) { + g_variant_get (value, "i", &brightness_value); + brightness_value = PERCENTAGE_TO_ABS (0, manager->kbd_brightness_max, + brightness_value); + if (upower_kbd_set_brightness (manager, brightness_value, error)) { + brightness_value = ABS_TO_PERCENTAGE (0, + manager->kbd_brightness_max, + manager->kbd_brightness_now); + backlight_iface_emit_changed (manager, GSD_POWER_DBUS_INTERFACE_KEYBOARD, brightness_value, "set property"); + return TRUE; + } else { + return FALSE; + } + } + + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "No such interface: %s", interface_name); + return FALSE; +} + +static gboolean +handle_set_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GVariant *value, + GError **error, gpointer user_data) +{ + GsdPowerManager *manager = GSD_POWER_MANAGER (user_data); + + /* Check session pointer as a proxy for whether the manager is in the + start or stop state */ + if (manager->session == NULL) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Manager is starting or stopping"); + return FALSE; + } + + if (g_strcmp0 (interface_name, GSD_POWER_DBUS_INTERFACE_SCREEN) == 0 || + g_strcmp0 (interface_name, GSD_POWER_DBUS_INTERFACE_KEYBOARD) == 0) { + return handle_set_property_other (manager, interface_name, property_name, value, error); + } else { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "No such interface: %s", interface_name); + return FALSE; + } +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + handle_get_property, + handle_set_property +}; + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *res, + GsdPowerManager *manager) +{ + GDBusConnection *connection; + GDBusInterfaceInfo **infos; + GError *error = NULL; + guint i; + + connection = g_bus_get_finish (res, &error); + if (connection == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Could not get session bus: %s", error->message); + g_error_free (error); + return; + } + + manager->connection = connection; + + infos = manager->introspection_data->interfaces; + for (i = 0; infos[i] != NULL; i++) { + g_dbus_connection_register_object (connection, + GSD_POWER_DBUS_PATH, + infos[i], + &interface_vtable, + manager, + NULL, + NULL); + } + + manager->name_id = g_bus_own_name_on_connection (connection, + GSD_POWER_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, + NULL, + NULL, + NULL); +} + +static void +register_manager_dbus (GsdPowerManager *manager) +{ + manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + g_assert (manager->introspection_data != NULL); + + g_bus_get (G_BUS_TYPE_SESSION, + manager->cancellable, + (GAsyncReadyCallback) on_bus_gotten, + manager); +} + +GsdPowerManager * +gsd_power_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_POWER_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + register_manager_dbus (manager_object); + } + return GSD_POWER_MANAGER (manager_object); +} diff --git a/plugins/power/gsd-power-manager.h b/plugins/power/gsd-power-manager.h new file mode 100644 index 0000000..75618de --- /dev/null +++ b/plugins/power/gsd-power-manager.h @@ -0,0 +1,47 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_POWER_MANAGER_H +#define __GSD_POWER_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_POWER_MANAGER (gsd_power_manager_get_type ()) +#define GSD_POWER_MANAGER_ERROR (gsd_power_manager_error_quark ()) + +G_DECLARE_FINAL_TYPE (GsdPowerManager, gsd_power_manager, GSD, POWER_MANAGER, GObject) + +enum +{ + GSD_POWER_MANAGER_ERROR_FAILED, + GSD_POWER_MANAGER_ERROR_NO_BACKLIGHT, +}; + +GQuark gsd_power_manager_error_quark (void); + +GsdPowerManager * gsd_power_manager_new (void); +gboolean gsd_power_manager_start (GsdPowerManager *manager, + GError **error); +void gsd_power_manager_stop (GsdPowerManager *manager); + +G_END_DECLS + +#endif /* __GSD_POWER_MANAGER_H */ diff --git a/plugins/power/gsdpowerconstants.py b/plugins/power/gsdpowerconstants.py new file mode 100644 index 0000000..a07798e --- /dev/null +++ b/plugins/power/gsdpowerconstants.py @@ -0,0 +1,16 @@ + +# File auto-generated from script http://git.gnome.org/browse/gnome-settings-daemon/tree/plugins/power/gsd-power-constants-update.pl + +# Modified by the GTK+ Team and others 1997-2012. See the AUTHORS +# file for a list of people on the GTK+ Team. See the ChangeLog +# files for a list of changes. These files are distributed with +# GTK+ at ftp://ftp.gtk.org/pub/gtk/. + +SCREENSAVER_TIMEOUT_BLANK = 15; +IDLE_DIM_BLANK_DISABLED_MIN = 60; +IDLE_DELAY_TO_IDLE_DIM_MULTIPLIER = 4.0/5.0; +MINIMUM_IDLE_DIM_DELAY = 10; +POWER_UP_TIME_ON_AC = 15; +GSD_MOCK_DEFAULT_BRIGHTNESS = 50; +GSD_MOCK_MAX_BRIGHTNESS = 100; +LID_CLOSE_SAFETY_TIMEOUT = 8; diff --git a/plugins/power/gsm-inhibitor-flag.h b/plugins/power/gsm-inhibitor-flag.h new file mode 100644 index 0000000..40698f9 --- /dev/null +++ b/plugins/power/gsm-inhibitor-flag.h @@ -0,0 +1,36 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __GSM_INHIBITOR_FLAG_H__ +#define __GSM_INHIBITOR_FLAG_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef enum { + GSM_INHIBITOR_FLAG_LOGOUT = 1 << 0, + GSM_INHIBITOR_FLAG_SWITCH_USER = 1 << 1, + GSM_INHIBITOR_FLAG_SUSPEND = 1 << 2, + GSM_INHIBITOR_FLAG_IDLE = 1 << 3, + GSM_INHIBITOR_FLAG_AUTOMOUNT = 1 << 4 +} GsmInhibitorFlag; + +G_END_DECLS + +#endif /* __GSM_INHIBITOR_FLAG_H__ */ diff --git a/plugins/power/gsm-manager-logout-mode.h b/plugins/power/gsm-manager-logout-mode.h new file mode 100644 index 0000000..7b51751 --- /dev/null +++ b/plugins/power/gsm-manager-logout-mode.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann <jmccann@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + + +#ifndef __GSM_MANAGER_LOGOUT_MODE_H +#define __GSM_MANAGER_LOGOUT_MODE_H + +G_BEGIN_DECLS + +typedef enum { + GSM_MANAGER_LOGOUT_MODE_NORMAL = 0, + GSM_MANAGER_LOGOUT_MODE_NO_CONFIRMATION, + GSM_MANAGER_LOGOUT_MODE_FORCE +} GsmManagerLogoutMode; + +G_END_DECLS + +#endif /* __GSM_MANAGER_LOGOUT_MODE_H */ diff --git a/plugins/power/gsm-presence-flag.h b/plugins/power/gsm-presence-flag.h new file mode 100644 index 0000000..f3bbca4 --- /dev/null +++ b/plugins/power/gsm-presence-flag.h @@ -0,0 +1,33 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2009 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __GSM_PRESENCE_FLAG_H__ +#define __GSM_PRESENCE_FLAG_H__ + +G_BEGIN_DECLS + +typedef enum { + GSM_PRESENCE_STATUS_AVAILABLE = 0, + GSM_PRESENCE_STATUS_INVISIBLE, + GSM_PRESENCE_STATUS_BUSY, + GSM_PRESENCE_STATUS_IDLE, +} GsmPresenceStatus; + +G_END_DECLS + +#endif /* __GSM_PRESENCE_FLAG_H__ */ diff --git a/plugins/power/main.c b/plugins/power/main.c new file mode 100644 index 0000000..4b851ed --- /dev/null +++ b/plugins/power/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_power_manager_new +#define START gsd_power_manager_start +#define STOP gsd_power_manager_stop +#define MANAGER GsdPowerManager +#include "gsd-power-manager.h" + +#include "daemon-skeleton-gtk.h" diff --git a/plugins/power/meson.build b/plugins/power/meson.build new file mode 100644 index 0000000..69e619f --- /dev/null +++ b/plugins/power/meson.build @@ -0,0 +1,150 @@ +sources = files( + 'gpm-common.c', + 'gsd-backlight.c', + 'gsd-power-manager.c', + 'main.c' +) + +deps = plugins_deps + [ + libcanberra_gtk_dep, + libcommon_dep, + libnotify_dep, + gio_unix_dep, + gnome_desktop_dep, + m_dep, + upower_glib_dep, + x11_dep, + dependency('xext') +] + +# required for gsd-power-enums-update, which we have +# to compile natively +native_deps = [ + dependency('glib-2.0', native: true), + dependency('gio-2.0', native:true) +] + +if host_is_linux + deps += gudev_dep +endif + +cflags += ['-DLIBEXECDIR="@0@"'.format(gsd_libexecdir)] + +gsd_power = executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, data_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) + +sources = files('gsd-power-enums-update.c') + +enums_headers = files( + 'gsm-inhibitor-flag.h', + 'gsm-presence-flag.h' +) + +enums = 'gsd-power-enums' + +sources += gnome.mkenums( + enums, + sources: enums_headers, + c_template: enums + '.c.in', + h_template: enums + '.h.in' +) + +gsd_power_enums_update = executable( + 'gsd-power-enums-update', + sources, + include_directories: top_inc, + dependencies: native_deps, + c_args: cflags, + native: true +) + +if host_is_linux + policy = 'org.gnome.settings-daemon.plugins.power.policy' + + policy_in = configure_file( + input: policy + '.in.in', + output: policy + '.in', + configuration: plugins_conf + ) + + i18n.merge_file( + policy, + input: policy_in, + output: policy, + po_dir: po_dir, + install: true, + install_dir: join_paths(gsd_datadir, 'polkit-1', 'actions') + ) + + sources = files( + 'gsd-backlight-helper.c', + ) + + deps = [ + ] + + executable( + 'gsd-backlight-helper', + sources, + include_directories: top_inc, + dependencies: deps, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir + ) +endif + +output = 'gsdpowerconstants.py' + +gsdpowerconstants_py = custom_target( + output, + input: 'gsd-power-constants.h', + output: output, + build_by_default: true, + command: [join_paths(meson.current_source_dir(), 'gsd-power-constants-update.pl'), '@INPUT@', '@OUTPUT@'] +) + +output = 'gsdpowerenums.py' + +gsdpowerenums_py = custom_target( + output, + output: output, + capture: true, + build_by_default: true, + command: [gsd_power_enums_update] +) + +test_py = find_program('test.py') + +envs = environment() +#envs.prepend('G_DEBUG', 'fatal-warnings') +envs.set('BUILDDIR', meson.current_build_dir()) +envs.set('TOP_BUILDDIR', meson.build_root()) +envs.set('LD_PRELOAD', 'libumockdev-preload.so.0') +envs.set('NO_AT_BRIDGE', '1') +envs.set('HAVE_SYSFS_BACKLIGHT', host_is_linux ? '1' : '0') + +if get_option('b_sanitize').split(',').contains('address') + # libasan needs to be loaded first; so we need to explicitly preload it + envs.set('POWER_LD_PRELOAD', 'libasan.so.5') +endif + +foreach i : [ 1, 2, 3, 4, 5, 6, 7, 8 ] + test( + 'test-power @0@/8'.format(i), + test_py, + args: [ 'PowerPluginTest@0@'.format(i) ], + env: envs, + # The first set of tests takes very long because of SCREENSAVER_TIMEOUT_BLANK + timeout: i == 1 ? 180 : 120 + ) +endforeach + diff --git a/plugins/power/org.gnome.settings-daemon.plugins.power.policy.in.in b/plugins/power/org.gnome.settings-daemon.plugins.power.policy.in.in new file mode 100644 index 0000000..f16300f --- /dev/null +++ b/plugins/power/org.gnome.settings-daemon.plugins.power.policy.in.in @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE policyconfig PUBLIC + "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" + "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd"> +<policyconfig> + + <!-- + Policy definitions for gnome-settings-daemon system-wide actions. + Copyright (c) 2010-2011 Richard Hughes <richard@hughsie.com> + --> + + <vendor>GNOME Settings Daemon</vendor> + <vendor_url>http://git.gnome.org/browse/gnome-settings-daemon</vendor_url> + <icon_name>battery</icon_name> + + <action id="org.gnome.settings-daemon.plugins.power.backlight-helper"> + <!-- SECURITY: + - A normal active user on the local machine does not need permission + to change the backlight brightness. + --> + <description>Modify the laptop brightness</description> + <message>Authentication is required to modify the laptop brightness</message> + <defaults> + <allow_any>no</allow_any> + <allow_inactive>no</allow_inactive> + <allow_active>yes</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.exec.path">@libexecdir@/gsd-backlight-helper</annotate> + </action> + +</policyconfig> + diff --git a/plugins/power/test-backlight-helper b/plugins/power/test-backlight-helper new file mode 100755 index 0000000..2dceacd --- /dev/null +++ b/plugins/power/test-backlight-helper @@ -0,0 +1,5 @@ +#!/bin/sh + +# Simulate a slow call and just write the given brightness value to the device +sleep 0.2 +echo "$2" >"$1/brightness" diff --git a/plugins/power/test.py b/plugins/power/test.py new file mode 100755 index 0000000..7bb190a --- /dev/null +++ b/plugins/power/test.py @@ -0,0 +1,1397 @@ +#!/usr/bin/python3 +'''GNOME settings daemon tests for power plugin.''' + +__author__ = 'Martin Pitt <martin.pitt@ubuntu.com>' +__copyright__ = '(C) 2013 Canonical Ltd.' +__license__ = 'GPL v2 or later' + +import unittest +import subprocess +import sys +import time +import math +import os +import os.path +import signal + +project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +builddir = os.environ.get('BUILDDIR', os.path.dirname(__file__)) + +sys.path.insert(0, os.path.join(project_root, 'tests')) +sys.path.insert(0, builddir) +import gsdtestcase +import gsdpowerconstants +import gsdpowerenums + +import dbus +from dbus.mainloop.glib import DBusGMainLoop + +DBusGMainLoop(set_as_default=True) + +import gi +gi.require_version('UPowerGlib', '1.0') +gi.require_version('UMockdev', '1.0') + +from gi.repository import Gio +from gi.repository import GLib +from gi.repository import UPowerGlib +from gi.repository import UMockdev + +class PowerPluginBase(gsdtestcase.GSDTestCase): + '''Test the power plugin''' + + COMMON_SUSPEND_METHODS=['Suspend', 'Hibernate', 'SuspendThenHibernate'] + + def setUp(self): + self.mock_external_monitor_file = os.path.join(self.workdir, 'GSD_MOCK_EXTERNAL_MONITOR') + os.environ['GSD_MOCK_EXTERNAL_MONITOR_FILE'] = self.mock_external_monitor_file + + self.check_logind_gnome_session() + self.start_logind() + self.daemon_death_expected = False + + + # Setup umockdev testbed + self.testbed = UMockdev.Testbed.new() + os.environ['UMOCKDEV_DIR'] = self.testbed.get_root_dir() + + # Create a mock backlight device + # Note that this function creates a different or even no backlight + # device based on the name of the test. + self.add_backlight() + + if 'HAVE_SYSFS_BACKLIGHT' in os.environ and os.environ['HAVE_SYSFS_BACKLIGHT'] == '1': + self.skip_sysfs_backlight = False + else: + self.skip_sysfs_backlight = True + + # start mock upowerd + (self.upowerd, self.obj_upower) = self.spawn_server_template( + 'upower', {'DaemonVersion': '0.99', 'OnBattery': True, 'LidIsClosed': False}, stdout=subprocess.PIPE) + gsdtestcase.set_nonblock(self.upowerd.stdout) + + # start mock gnome-shell screensaver + (self.screensaver, self.obj_screensaver) = self.spawn_server_template( + 'gnome_screensaver', stdout=subprocess.PIPE) + gsdtestcase.set_nonblock(self.screensaver.stdout) + + self.session_log_write = open(os.path.join(self.workdir, 'gnome-session.log'), 'wb', buffering=0) + self.session = subprocess.Popen(['gnome-session', '-f', + '-a', os.path.join(self.workdir, 'autostart'), + '--session=dummy', '--debug'], + stdout=self.session_log_write, + stderr=subprocess.STDOUT) + + # wait until the daemon is on the bus + try: + self.wait_for_bus_object('org.gnome.SessionManager', + '/org/gnome/SessionManager') + except: + # on failure, print log + with open(self.session_log_write.name) as f: + print('----- session log -----\n%s\n------' % f.read()) + raise + + self.session_log = open(self.session_log_write.name, 'rb', buffering=0) + + self.obj_session_mgr = self.session_bus_con.get_object( + 'org.gnome.SessionManager', '/org/gnome/SessionManager') + + self.start_mutter() + + # Set up the gnome-session presence + obj_session_presence = self.session_bus_con.get_object( + 'org.gnome.SessionManager', '/org/gnome/SessionManager/Presence') + self.obj_session_presence_props = dbus.Interface(obj_session_presence, dbus.PROPERTIES_IFACE) + + # ensure that our tests don't lock the screen when the screensaver + # gets active + self.settings_screensaver = Gio.Settings(schema_id='org.gnome.desktop.screensaver') + self.settings_screensaver['lock-enabled'] = False + + # Ensure we set up the external monitor state + self.set_has_external_monitor(False) + + self.settings_gsd_power = Gio.Settings(schema_id='org.gnome.settings-daemon.plugins.power') + + Gio.Settings.sync() + self.plugin_log_write = open(os.path.join(self.workdir, 'plugin_power.log'), 'wb', buffering=0) + # avoid painfully long delays of actions for tests + env = os.environ.copy() + # Disable PulseAudio output from libcanberra + env['CANBERRA_DRIVER'] = 'null' + + # Use dummy script as testing backlight helper + env['GSD_BACKLIGHT_HELPER'] = os.path.join (project_root, 'plugins', 'power', 'test-backlight-helper') + if 'POWER_LD_PRELOAD' in env: + if 'LD_PRELOAD' in env and env['LD_PRELOAD']: + env['LD_PRELOAD'] = ':'.join((env['POWER_LD_PRELOAD'], env['LD_PRELOAD'])) + else: + env['LD_PRELOAD'] = env['POWER_LD_PRELOAD'] + + # We need to redirect stdout to grab the debug messages. + # stderr is not needed by the testing infrastructure but is useful to + # see warnings and errors. + self.daemon = subprocess.Popen( + [os.path.join(builddir, 'gsd-power'), '--verbose'], + stdout=self.plugin_log_write, + env=env) + + # you can use this for reading the current daemon log in tests + self.plugin_log = open(self.plugin_log_write.name, 'rb', buffering=0) + + # wait until plugin is ready + timeout = 100 + while timeout > 0: + time.sleep(0.1) + timeout -= 1 + log = self.plugin_log.read() + if b'System inhibitor fd is' in log: + break + + # always start with zero idle time + self.reset_idle_timer() + + # flush notification log + self.p_notify.stdout.read() + + def tearDown(self): + + daemon_running = self.daemon.poll() == None + if daemon_running: + self.daemon.terminate() + self.daemon.wait() + self.plugin_log.close() + self.plugin_log_write.flush() + self.plugin_log_write.close() + + self.upowerd.terminate() + self.upowerd.wait() + self.screensaver.terminate() + self.screensaver.wait() + self.stop_session() + self.stop_mutter() + self.stop_logind() + + # reset all changed gsettings, so that tests are independent from each + # other + for schema in [self.settings_gsd_power, self.settings_session, self.settings_screensaver]: + for k in schema.list_keys(): + schema.reset(k) + Gio.Settings.sync() + + try: + os.unlink(self.mock_external_monitor_file) + except OSError: + pass + + del self.testbed + + # we check this at the end so that the other cleanup always happens + self.assertTrue(daemon_running or self.daemon_death_expected, 'daemon died during the test') + + def stop_session(self): + '''Stop GNOME session''' + + assert self.session + self.session.terminate() + self.session.wait() + + self.session_log_write.flush() + self.session_log_write.close() + self.session_log.close() + + def check_logind_gnome_session(self): + '''Check that gnome-session is built with logind support''' + + path = GLib.find_program_in_path ('gnome-session') + assert(path) + (success, data) = GLib.file_get_contents (path) + lines = data.split(b'\n') + new_path = None + for line in lines: + items = line.split() + if items and items[0] == b'exec': + new_path = items[1] + if not new_path: + self.fail("could not get gnome-session's real path from %s" % path) + path = new_path + ldd = subprocess.Popen(['ldd', path], stdout=subprocess.PIPE) + out = ldd.communicate()[0] + if not b'libsystemd.so.0' in out: + self.fail('gnome-session is not built with logind support') + + def get_status(self): + return self.obj_session_presence_props.Get('org.gnome.SessionManager.Presence', 'status') + + def backlight_defaults(self): + # Hack to modify the brightness defaults before starting gsd-power. + # The alternative would be to create two separate test files. + if 'no_backlight' in self.id(): + return None, None + elif 'legacy_brightness' in self.id(): + return 15, 15 + else: + return 100, 50 + + def add_backlight(self, _type="raw"): + max_brightness, brightness = self.backlight_defaults() + + if max_brightness is None: + self.backlight = None + return + + # Undo mangling done in GSD + if max_brightness >= 99: + max_brightness += 1 + brightness += 1 + + # This needs to be done before starting gsd-power! + self.backlight = self.testbed.add_device('backlight', 'mock_backlight', None, + ['type', _type, + 'max_brightness', str(max_brightness), + 'brightness', str(brightness)], + []) + + def get_brightness(self): + max_brightness = int(open(os.path.join(self.testbed.get_root_dir() + self.backlight, 'max_brightness')).read()) + + # self.backlight contains the leading slash, so os.path.join doesn't quite work + res = int(open(os.path.join(self.testbed.get_root_dir() + self.backlight, 'brightness')).read()) + # Undo mangling done in GSD + if max_brightness >= 99: + res -= 1 + return res + + def set_has_external_monitor(self, external): + if external: + val = b'1' + else: + val = b'0' + GLib.file_set_contents (self.mock_external_monitor_file, val) + + def set_composite_battery_discharging(self, icon='battery-good-symbolic'): + self.obj_upower.SetupDisplayDevice( + UPowerGlib.DeviceKind.BATTERY, + UPowerGlib.DeviceState.DISCHARGING, + 50., 50., 100., # 50%, charge 50 of 100 + 0.01, 600, 0, # Discharge rate 0.01 with 600 seconds remaining, 0 time to full + True, # present + icon, UPowerGlib.DeviceLevel.NONE + ) + + def set_composite_battery_critical(self, icon='battery-caution-symbolic'): + self.obj_upower.SetupDisplayDevice( + UPowerGlib.DeviceKind.BATTERY, + UPowerGlib.DeviceState.DISCHARGING, + 2., 2., 100., # 2%, charge 2 of 100 + 0.01, 60, 0, # Discharge rate 0.01 with 60 seconds remaining, 0 time to full + True, # present + icon, UPowerGlib.DeviceLevel.CRITICAL + ) + + def check_for_logout(self, timeout): + '''Check that logout is requested. + + Fail after the given timeout. + ''' + # check that it request logout + while timeout > 0: + time.sleep(1) + timeout -= 1 + # check that it requested logout + log = self.session_log.read() + if log is None: + continue + + if log and (b'GsmManager: requesting logout' in log): + break + else: + self.fail('timed out waiting for gnome-session logout call') + + def check_no_logout(self, seconds): + '''Check that no logout is requested in the given time''' + + # wait for specified time to ensure it didn't do anything + time.sleep(seconds) + # check that it did not logout + log = self.session_log.read() + if log: + self.assertFalse(b'GsmManager: requesting logout' in log, 'unexpected logout request') + + def check_for_suspend(self, timeout, methods=COMMON_SUSPEND_METHODS): + '''Check that one of the given suspend methods are requested. Default + methods are Suspend() or Hibernate() but also HibernateThenSuspend() + is valid. + + Fail after the given timeout. + ''' + + # Create a list of byte string needles to search for + needles = [' {} '.format(m).encode('ascii') for m in methods] + + suspended = False + + # check that it request suspend + while timeout > 0: + time.sleep(1) + timeout -= 1 + # check that it requested suspend + log = self.logind.stdout.read() + if log is None: + continue + + for n in needles: + if n in log: + suspended = True + break + + if suspended: + break + + if not suspended: + self.fail('timed out waiting for logind suspend call, methods: %s' % ', '.join(methods)) + + def check_for_lid_inhibited(self, timeout=0): + '''Check that the lid inhibitor has been added. + + Fail after the given timeout. + ''' + self.check_plugin_log('Adding lid switch system inhibitor', timeout, + 'Timed out waiting for lid inhibitor') + + def check_for_lid_uninhibited(self, timeout=0): + '''Check that the lid inhibitor has been dropped. + + Fail after the given timeout. + ''' + self.check_plugin_log('uninhibiting lid close', timeout, + 'Timed out waiting for lid uninhibition') + + def check_no_lid_uninhibited(self, timeout=0): + '''Check that the lid inhibitor has been dropped. + + Fail after the given timeout. + ''' + time.sleep(timeout) + # check that it requested uninhibition + log = self.plugin_log.read() + + if b'uninhibiting lid close' in log: + self.fail('lid uninhibit should not have happened') + + def check_no_suspend(self, seconds, methods=COMMON_SUSPEND_METHODS): + '''Check that no Suspend or Hibernate is requested in the given time''' + + # wait for specified time to ensure it didn't do anything + time.sleep(seconds) + # check that it did not suspend or hibernate + log = self.logind.stdout.read() + if log is None: + return + + for m in methods: + needle = ' {} '.format(m).encode('ascii') + + self.assertFalse(needle in log, 'unexpected %s request' % m) + + def check_suspend_no_hibernate(self, seconds): + '''Check that Suspend was requested and not Hibernate, in the given time''' + + # wait for specified time to ensure it didn't do anything + time.sleep(seconds) + # check that it did suspend and didn't hibernate + log = self.logind.stdout.read() + if log: + self.assertTrue(b' Suspend' in log, 'missing Suspend request') + self.assertFalse(b' Hibernate' in log, 'unexpected Hibernate request') + + def check_plugin_log(self, needle, timeout=0, failmsg=None): + '''Check that needle is found in the log within the given timeout. + Returns immediately when found. + + Fail after the given timeout. + ''' + if type(needle) == str: + needle = needle.encode('ascii') + # Fast path if the message was already logged + log = self.plugin_log.read() + if needle in log: + return + + while timeout > 0: + time.sleep(0.5) + timeout -= 0.5 + + # read new data (lines) from the log + log = self.plugin_log.read() + if needle in log: + break + else: + if failmsg is not None: + self.fail(failmsg) + else: + self.fail('timed out waiting for needle "%s"' % needle) + + def check_no_dim(self, seconds): + '''Check that mode is not set to dim in the given time''' + + # wait for specified time to ensure it didn't do anything + time.sleep(seconds) + # check that we don't dim + log = self.plugin_log.read() + if log: + self.assertFalse(b'Doing a state transition: dim' in log, 'unexpected dim request') + + def check_dim(self, timeout): + '''Check that mode is set to dim in the given time''' + + self.check_plugin_log('Doing a state transition: dim', timeout, + 'timed out waiting for dim') + + def check_undim(self, timeout): + '''Check that mode is set to normal in the given time''' + + self.check_plugin_log('Doing a state transition: normal', timeout, + 'timed out waiting for normal mode') + + def check_blank(self, timeout): + '''Check that blank is requested. + + Fail after the given timeout. + ''' + + self.check_plugin_log('TESTSUITE: Blanked screen', timeout, + 'timed out waiting for blank') + + def check_unblank(self, timeout): + '''Check that unblank is requested. + + Fail after the given timeout. + ''' + + self.check_plugin_log('TESTSUITE: Unblanked screen', timeout, + 'timed out waiting for unblank') + + def check_no_blank(self, seconds): + '''Check that no blank is requested in the given time''' + + # wait for specified time to ensure it didn't blank + time.sleep(seconds) + # check that it did not blank + log = self.plugin_log.read() + self.assertFalse(b'TESTSUITE: Blanked screen' in log, 'unexpected blank request') + + def check_no_unblank(self, seconds): + '''Check that no unblank is requested in the given time''' + + # wait for specified time to ensure it didn't unblank + time.sleep(seconds) + # check that it did not unblank + log = self.plugin_log.read() + self.assertFalse(b'TESTSUITE: Unblanked screen' in log, 'unexpected unblank request') + +class PowerPluginTest1(PowerPluginBase): + def test_screensaver(self): + # Note that the screensaver mock object + # doesn't know how to get out of being active, + # be it if the lock is disabled, or not. + + self.obj_screensaver.Lock() + # 0.3 second animation + time.sleep(1) + self.assertTrue(self.obj_screensaver.GetActive(), 'screensaver not turned on') + + # blank is supposed to happen straight away + self.check_blank(2) + + # Wait a bit for the active watch to be registered through dbus, then + # fake user activity and check that the screen is unblanked. + time.sleep(0.5) + self.reset_idle_timer() + self.check_unblank(2) + + # Check for no blank before the normal blank timeout + self.check_no_blank(gsdpowerconstants.SCREENSAVER_TIMEOUT_BLANK - 4) + self.assertTrue(self.obj_screensaver.GetActive(), 'screensaver not turned on') + + # and check for blank after the blank timeout + self.check_blank(10) + + # Wait a bit for the active watch to be registered through dbus, then + # fake user activity and check that the screen is unblanked. + time.sleep(0.5) + self.reset_idle_timer() + self.check_unblank(2) + + # check no blank and then blank + self.check_no_blank(gsdpowerconstants.SCREENSAVER_TIMEOUT_BLANK - 4) + self.assertTrue(self.obj_screensaver.GetActive(), 'screensaver not turned on') + self.check_blank(10) + + def test_sleep_inactive_blank(self): + '''screensaver/blank interaction''' + + # create suspend inhibitor which should have no effect on the idle + inhibit_id = self.obj_session_mgr.Inhibit( + 'testsuite', dbus.UInt32(0), 'for testing', + dbus.UInt32(gsdpowerenums.GSM_INHIBITOR_FLAG_SUSPEND), + dbus_interface='org.gnome.SessionManager') + + self.obj_screensaver.SetActive(True) + self.assertTrue(self.obj_screensaver.GetActive(), 'screensaver not turned on') + + # blank is supposed to happen straight away + self.check_blank(2) + + # Wait a bit for the active watch to be registered through dbus, then + # fake user activity and check that the screen is unblanked. + time.sleep(0.5) + self.reset_idle_timer() + self.check_unblank(2) + if not self.skip_sysfs_backlight: + self.assertTrue(self.get_brightness() == gsdpowerconstants.GSD_MOCK_DEFAULT_BRIGHTNESS , 'incorrect unblanked brightness (%d != %d)' % (self.get_brightness(), gsdpowerconstants.GSD_MOCK_DEFAULT_BRIGHTNESS)) + + # Check for no blank before the normal blank timeout + self.check_no_blank(gsdpowerconstants.SCREENSAVER_TIMEOUT_BLANK - 4) + self.assertTrue(self.obj_screensaver.GetActive(), 'screensaver not turned on') + + # and check for blank after the blank timeout + self.check_blank(10) + + # Drop inhibitor + self.obj_session_mgr.Uninhibit(dbus.UInt32(inhibit_id), + dbus_interface='org.gnome.SessionManager') + +class PowerPluginTest2(PowerPluginBase): + def test_screensaver_no_unblank(self): + '''Ensure the screensaver is not unblanked for new inhibitors.''' + + # Lower idle delay a lot + self.settings_session['idle-delay'] = 1 + + # Bring down the screensaver + self.obj_screensaver.SetActive(True) + self.assertTrue(self.obj_screensaver.GetActive(), 'screensaver not turned on') + + # Check that we blank + self.check_blank(2) + + # Create the different possible inhibitors + inhibit_id = self.obj_session_mgr.Inhibit( + 'testsuite', dbus.UInt32(0), 'for testing', + dbus.UInt32(gsdpowerenums.GSM_INHIBITOR_FLAG_IDLE | gsdpowerenums.GSM_INHIBITOR_FLAG_SUSPEND | gsdpowerenums.GSM_INHIBITOR_FLAG_LOGOUT), + dbus_interface='org.gnome.SessionManager') + + self.check_no_unblank(2) + + # Drop inhibitor + self.obj_session_mgr.Uninhibit(dbus.UInt32(inhibit_id), + dbus_interface='org.gnome.SessionManager') + + self.check_no_unblank(2) + + def test_session_idle_delay(self): + '''verify that session idle delay works as expected when changed''' + + # Verify that idle is set after 5 seconds + self.settings_session['idle-delay'] = 5 + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_AVAILABLE) + time.sleep(7) + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_IDLE) + + # Raise the idle delay, and see that we stop being idle + # and get idle again after the timeout + self.settings_session['idle-delay'] = 10 + # Resolve possible race condition, see also https://gitlab.gnome.org/GNOME/mutter/issues/113 + time.sleep(0.2) + self.reset_idle_timer() + time.sleep(5) + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_AVAILABLE) + time.sleep(10) + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_IDLE) + + # Lower the delay again, and see that we get idle as we should + self.settings_session['idle-delay'] = 5 + # Resolve possible race condition, see also https://gitlab.gnome.org/GNOME/mutter/issues/113 + time.sleep(0.2) + self.reset_idle_timer() + time.sleep(2) + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_AVAILABLE) + time.sleep(5) + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_IDLE) + + def test_idle_time_reset_on_resume(self): + '''Check that the IDLETIME is reset when resuming''' + + self.settings_screensaver['lock-enabled'] = False + + # Go idle + self.settings_session['idle-delay'] = 5 + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_AVAILABLE) + time.sleep(7) + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_IDLE) + + # Go to sleep + self.logind_obj.EmitSignal('', 'PrepareForSleep', 'b', [True], dbus_interface='org.freedesktop.DBus.Mock') + time.sleep(1) + + # Wake up + self.logind_obj.EmitSignal('', 'PrepareForSleep', 'b', [False], dbus_interface='org.freedesktop.DBus.Mock') + time.sleep(1) + + # And check we're not idle + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_AVAILABLE) + +class PowerPluginTest3(PowerPluginBase): + def test_sleep_inactive_battery(self): + '''sleep-inactive-battery-timeout''' + + self.settings_session['idle-delay'] = 2 + self.settings_gsd_power['sleep-inactive-battery-timeout'] = 5 + self.settings_gsd_power['sleep-inactive-battery-type'] = 'suspend' + + # wait for idle delay; should not yet suspend + self.check_no_suspend(2) + + # suspend should happen after inactive sleep timeout + 1 s notification + # delay + 1 s error margin + self.check_for_suspend(7) + + def _test_suspend_no_hibernate(self): + '''suspend-no-hibernate''' + + self.settings_session['idle-delay'] = 2 + self.settings_gsd_power['sleep-inactive-battery-timeout'] = 5 + # Hibernate isn't possible, so it should end up suspending + # FIXME + self.settings_gsd_power['critical-battery-action'] = 'hibernate' + + # wait for idle delay; should not yet hibernate + self.check_no_suspend(2) + + # suspend should happen after inactive sleep timeout + 1 s notification + # delay + 1 s error margin + self.check_suspend_no_hibernate(7) + + def test_sleep_inhibition(self): + '''Does not sleep under idle inhibition''' + + idle_delay = round(gsdpowerconstants.MINIMUM_IDLE_DIM_DELAY / gsdpowerconstants.IDLE_DELAY_TO_IDLE_DIM_MULTIPLIER) + + self.settings_session['idle-delay'] = idle_delay + self.settings_gsd_power['sleep-inactive-battery-timeout'] = 5 + self.settings_gsd_power['sleep-inactive-battery-type'] = 'suspend' + + # create inhibitor + inhibit_id = self.obj_session_mgr.Inhibit( + 'testsuite', dbus.UInt32(0), 'for testing', + dbus.UInt32(gsdpowerenums.GSM_INHIBITOR_FLAG_IDLE | gsdpowerenums.GSM_INHIBITOR_FLAG_SUSPEND), + dbus_interface='org.gnome.SessionManager') + self.check_no_suspend(idle_delay + 2) + self.check_no_dim(0) + + # Check that we didn't go to idle either + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_AVAILABLE) + + self.obj_session_mgr.Uninhibit(dbus.UInt32(inhibit_id), + dbus_interface='org.gnome.SessionManager') + +class PowerPluginTest4(PowerPluginBase): + def test_lock_on_lid_close(self): + '''Check that we do lock on lid closing, if the machine will not suspend''' + + self.settings_screensaver['lock-enabled'] = True + + # create inhibitor + inhibit_id = self.obj_session_mgr.Inhibit( + 'testsuite', dbus.UInt32(0), 'for testing', + dbus.UInt32(gsdpowerenums.GSM_INHIBITOR_FLAG_SUSPEND), + dbus_interface='org.gnome.SessionManager') + + time.sleep (gsdpowerconstants.LID_CLOSE_SAFETY_TIMEOUT) + + # Close the lid + self.obj_upower.Set('org.freedesktop.UPower', 'LidIsClosed', True) + self.obj_upower.EmitSignal('', 'Changed', '', [], dbus_interface='org.freedesktop.DBus.Mock') + + # Check that we've blanked + time.sleep(2) + self.assertTrue(self.obj_screensaver.GetActive(), 'screensaver not turned on') + self.check_blank(2) + + # Drop the inhibit and see whether we suspend + self.obj_session_mgr.Uninhibit(dbus.UInt32(inhibit_id), + dbus_interface='org.gnome.SessionManager') + # At this point logind should suspend for us + self.settings_screensaver['lock-enabled'] = False + + def test_blank_on_lid_close(self): + '''Check that we do blank on lid closing, if the machine will not suspend''' + + # create inhibitor + inhibit_id = self.obj_session_mgr.Inhibit( + 'testsuite', dbus.UInt32(0), 'for testing', + dbus.UInt32(gsdpowerenums.GSM_INHIBITOR_FLAG_SUSPEND), + dbus_interface='org.gnome.SessionManager') + + time.sleep (gsdpowerconstants.LID_CLOSE_SAFETY_TIMEOUT) + + # Close the lid + self.obj_upower.Set('org.freedesktop.UPower', 'LidIsClosed', True) + self.obj_upower.EmitSignal('', 'Changed', '', [], dbus_interface='org.freedesktop.DBus.Mock') + + # Check that we've blanked + self.check_blank(4) + + # Drop the inhibit and see whether we suspend + self.obj_session_mgr.Uninhibit(dbus.UInt32(inhibit_id), + dbus_interface='org.gnome.SessionManager') + # At this point logind should suspend for us + + def test_unblank_on_lid_open(self): + '''Check that we do unblank on lid opening, if the machine will not suspend''' + + # create inhibitor + inhibit_id = self.obj_session_mgr.Inhibit( + 'testsuite', dbus.UInt32(0), 'for testing', + dbus.UInt32(gsdpowerenums.GSM_INHIBITOR_FLAG_SUSPEND), + dbus_interface='org.gnome.SessionManager') + + time.sleep (gsdpowerconstants.LID_CLOSE_SAFETY_TIMEOUT) + + # Close the lid + self.obj_upower.Set('org.freedesktop.UPower', 'LidIsClosed', True) + self.obj_upower.EmitSignal('', 'Changed', '', [], dbus_interface='org.freedesktop.DBus.Mock') + + # Check that we've blanked + self.check_blank(2) + + # Reopen the lid + self.obj_upower.Set('org.freedesktop.UPower', 'LidIsClosed', False) + self.obj_upower.EmitSignal('', 'Changed', '', [], dbus_interface='org.freedesktop.DBus.Mock') + + # Check for unblanking + self.check_unblank(2) + + # Drop the inhibit + self.obj_session_mgr.Uninhibit(dbus.UInt32(inhibit_id), + dbus_interface='org.gnome.SessionManager') + +class PowerPluginTest5(PowerPluginBase): + def test_dim(self): + '''Check that we do go to dim''' + + # Wait and flush log + time.sleep (gsdpowerconstants.LID_CLOSE_SAFETY_TIMEOUT + 1) + self.plugin_log.read() + + idle_delay = math.ceil(gsdpowerconstants.MINIMUM_IDLE_DIM_DELAY / gsdpowerconstants.IDLE_DELAY_TO_IDLE_DIM_MULTIPLIER) + self.reset_idle_timer() + + self.settings_session['idle-delay'] = idle_delay + self.settings_gsd_power['sleep-inactive-battery-timeout'] = idle_delay + 1 + self.settings_gsd_power['sleep-inactive-battery-type'] = 'suspend' + # This is an absolute percentage, and our brightness is 0..100 + dim_level = self.settings_gsd_power['idle-brightness']; + + # Check that we're not idle + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_AVAILABLE) + + # Wait and check we're not idle, but dimmed + self.check_dim(gsdpowerconstants.MINIMUM_IDLE_DIM_DELAY + 1) + # Give time for the brightness to change + time.sleep(2) + if not self.skip_sysfs_backlight: + level = self.get_brightness(); + self.assertTrue(level == dim_level, 'incorrect dim brightness (%d != %d)' % (level, dim_level)) + + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_AVAILABLE) + + # Bring down the screensaver + self.obj_screensaver.SetActive(True) + self.assertTrue(self.obj_screensaver.GetActive(), 'screensaver not turned on') + + # Check that we blank + self.check_blank(2) + + # Go to sleep + self.logind_obj.EmitSignal('', 'PrepareForSleep', 'b', [True], dbus_interface='org.freedesktop.DBus.Mock') + time.sleep(1) + + # Wake up + self.logind_obj.EmitSignal('', 'PrepareForSleep', 'b', [False], dbus_interface='org.freedesktop.DBus.Mock') + time.sleep(1) + + # And check that we have the pre-dim brightness + if not self.skip_sysfs_backlight: + self.assertTrue(self.get_brightness() == gsdpowerconstants.GSD_MOCK_DEFAULT_BRIGHTNESS , 'incorrect unblanked brightness (%d != %d)' % (self.get_brightness(), gsdpowerconstants.GSD_MOCK_DEFAULT_BRIGHTNESS)) + + def test_lid_close_inhibition(self): + '''Check that we correctly inhibit suspend with an external monitor''' + + # Wait and flush log + time.sleep (gsdpowerconstants.LID_CLOSE_SAFETY_TIMEOUT + 1) + self.plugin_log.read() + + # Add an external monitor + self.set_has_external_monitor(True) + self.check_for_lid_inhibited(1) + + # Check that we do not uninhibit with the external monitor attached + self.check_no_lid_uninhibited(gsdpowerconstants.LID_CLOSE_SAFETY_TIMEOUT + 1) + + # Close the lid + self.obj_upower.Set('org.freedesktop.UPower', 'LidIsClosed', True) + self.obj_upower.EmitSignal('', 'Changed', '', [], dbus_interface='org.freedesktop.DBus.Mock') + time.sleep(0.5) + + # Unplug the external monitor + self.set_has_external_monitor(False) + + # Check that no action happens during the safety time minus 1 second + self.check_no_lid_uninhibited(gsdpowerconstants.LID_CLOSE_SAFETY_TIMEOUT - 1) + # Check that we're uninhibited after the safety time + self.check_for_lid_uninhibited(4) + +class PowerPluginTest6(PowerPluginBase): + def test_notify_critical_battery(self): + '''action on critical battery''' + + self.set_composite_battery_discharging() + + time.sleep(2) + + self.set_composite_battery_critical() + + # Check that it was picked up + self.check_plugin_log('EMIT: charge-critical', 2) + + # Wait a bit longer to ensure event has been fired + time.sleep(0.5) + # we should have gotten a notification now + notify_log = self.p_notify.stdout.read() + + # verify notification + self.assertRegex(notify_log, b'[0-9.]+ Notify "Power" .* "battery-caution-symbolic" ".*battery critical.*"') + + def test_notify_critical_battery_on_start(self): + '''action on critical battery on startup''' + + self.set_composite_battery_critical() + + # Check that it was picked up + self.check_plugin_log('EMIT: charge-critical', 2) + + time.sleep(0.5) + + # we should have gotten a notification by now + notify_log = self.p_notify.stdout.read() + + # verify notification + self.assertRegex(notify_log, b'[0-9.]+ Notify "Power" .* "battery-caution-symbolic" ".*battery critical.*"') + + def test_notify_device_battery(self): + '''critical power level notification for device batteries''' + + # Set internal battery to discharging + self.set_composite_battery_discharging() + + # Add a device battery + bat2_path = '/org/freedesktop/UPower/devices/' + 'mock_MOUSE_BAT1' + self.obj_upower.AddObject(bat2_path, + 'org.freedesktop.UPower.Device', + { + 'PowerSupply': dbus.Boolean(False, variant_level=1), + 'IsPresent': dbus.Boolean(True, variant_level=1), + 'Model': dbus.String('Bat1', variant_level=1), + 'Percentage': dbus.Double(40.0, variant_level=1), + 'TimeToEmpty': dbus.Int64(1600, variant_level=1), + 'EnergyFull': dbus.Double(100.0, variant_level=1), + 'Energy': dbus.Double(40.0, variant_level=1), + 'State': dbus.UInt32(UPowerGlib.DeviceState.DISCHARGING, variant_level=1), + 'Type': dbus.UInt32(UPowerGlib.DeviceKind.MOUSE, variant_level=1), + 'WarningLevel': dbus.UInt32(UPowerGlib.DeviceLevel.NONE, variant_level=1), + }, dbus.Array([], signature='(ssss)')) + + obj_bat2 = self.system_bus_con.get_object('org.freedesktop.UPower', bat2_path) + self.obj_upower.EmitSignal('', 'DeviceAdded', 'o', [bat2_path], + dbus_interface='org.freedesktop.DBus.Mock') + time.sleep(1) + + # now change the mouse battery to critical charge + obj_bat2.Set('org.freedesktop.UPower.Device', 'TimeToEmpty', + dbus.Int64(30, variant_level=1), + dbus_interface=dbus.PROPERTIES_IFACE) + obj_bat2.Set('org.freedesktop.UPower.Device', 'Energy', + dbus.Double(0.5, variant_level=1), + dbus_interface=dbus.PROPERTIES_IFACE) + obj_bat2.Set('org.freedesktop.UPower.Device', 'WarningLevel', + dbus.UInt32(UPowerGlib.DeviceLevel.CRITICAL, variant_level=1), + dbus_interface=dbus.PROPERTIES_IFACE) + obj_bat2.EmitSignal('', 'Changed', '', [], dbus_interface='org.freedesktop.DBus.Mock') + self.obj_upower.EmitSignal('', 'DeviceChanged', 'o', [bat2_path], + dbus_interface='org.freedesktop.DBus.Mock') + + self.check_plugin_log('EMIT: charge-critical', 2) + time.sleep(0.5) + + # we should have gotten a notification by now + notify_log = self.p_notify.stdout.read() + + # verify notification + self.assertRegex(notify_log, b'[0-9.]+ Notify "Power" .* ".*" ".*Wireless mouse .*low.* power.*\([0-9.]+%\).*"') + + def test_notify_device_spam(self): + '''no repeat notifications for device batteries''' + + # Set internal battery to discharging + self.set_composite_battery_discharging() + + # Add a device battery + bat2_path = '/org/freedesktop/UPower/devices/' + 'mock_MOUSE_BAT1' + self.obj_upower.AddObject(bat2_path, + 'org.freedesktop.UPower.Device', + { + 'PowerSupply': dbus.Boolean(False, variant_level=1), + 'IsPresent': dbus.Boolean(True, variant_level=1), + 'Model': dbus.String('Bat1', variant_level=1), + 'Serial': dbus.String('12345678', variant_level=1), + 'Percentage': dbus.Double(10.0, variant_level=1), + 'State': dbus.UInt32(UPowerGlib.DeviceState.DISCHARGING, variant_level=1), + 'Type': dbus.UInt32(UPowerGlib.DeviceKind.MOUSE, variant_level=1), + 'WarningLevel': dbus.UInt32(UPowerGlib.DeviceLevel.LOW, variant_level=1), + }, dbus.Array([], signature='(ssss)')) + + obj_bat2 = self.system_bus_con.get_object('org.freedesktop.UPower', bat2_path) + self.obj_upower.EmitSignal('', 'DeviceAdded', 'o', [bat2_path], + dbus_interface='org.freedesktop.DBus.Mock') + time.sleep(1) + + self.check_plugin_log('EMIT: charge-low', 2) + time.sleep(0.5) + + # we should have gotten a notification by now + notify_log = self.p_notify.stdout.read() + + # verify notification + self.assertRegex(notify_log, b'[0-9.]+ Notify "Power" .* ".*" ".*Wireless mouse .*low.* power.*\([0-9.]+%\).*"') + + # Disconnect mouse + self.obj_upower.RemoveObject(bat2_path) + time.sleep(0.5) + + # Reconnect mouse + self.obj_upower.AddObject(bat2_path, + 'org.freedesktop.UPower.Device', + { + 'PowerSupply': dbus.Boolean(False, variant_level=1), + 'IsPresent': dbus.Boolean(True, variant_level=1), + 'Model': dbus.String('Bat1', variant_level=1), + 'Serial': dbus.String('12345678', variant_level=1), + 'Percentage': dbus.Double(10.0, variant_level=1), + 'State': dbus.UInt32(UPowerGlib.DeviceState.DISCHARGING, variant_level=1), + 'Type': dbus.UInt32(UPowerGlib.DeviceKind.MOUSE, variant_level=1), + 'WarningLevel': dbus.UInt32(UPowerGlib.DeviceLevel.LOW, variant_level=1), + }, dbus.Array([], signature='(ssss)')) + + obj_bat2 = self.system_bus_con.get_object('org.freedesktop.UPower', bat2_path) + self.obj_upower.EmitSignal('', 'DeviceAdded', 'o', [bat2_path], + dbus_interface='org.freedesktop.DBus.Mock') + time.sleep(1) + + # we shouldn't have gotten a notification by now + notify_log = self.p_notify.stdout.read() + self.assertIsNone(notify_log) + + # Disconnect mouse + self.obj_upower.RemoveObject(bat2_path) + time.sleep(0.5) + + # Reconnect mouse with critical battery level + self.obj_upower.AddObject(bat2_path, + 'org.freedesktop.UPower.Device', + { + 'PowerSupply': dbus.Boolean(False, variant_level=1), + 'IsPresent': dbus.Boolean(True, variant_level=1), + 'Model': dbus.String('Bat1', variant_level=1), + 'Serial': dbus.String('12345678', variant_level=1), + 'Percentage': dbus.Double(5.0, variant_level=1), + 'State': dbus.UInt32(UPowerGlib.DeviceState.DISCHARGING, variant_level=1), + 'Type': dbus.UInt32(UPowerGlib.DeviceKind.MOUSE, variant_level=1), + 'WarningLevel': dbus.UInt32(UPowerGlib.DeviceLevel.CRITICAL, variant_level=1), + }, dbus.Array([], signature='(ssss)')) + + obj_bat2 = self.system_bus_con.get_object('org.freedesktop.UPower', bat2_path) + self.obj_upower.EmitSignal('', 'DeviceAdded', 'o', [bat2_path], + dbus_interface='org.freedesktop.DBus.Mock') + time.sleep(1) + + # Verify new warning + self.check_plugin_log('EMIT: charge-critical', 2) + time.sleep(0.5) + + # we should have gotten a notification by now + notify_log = self.p_notify.stdout.read() + + # verify notification + self.assertRegex(notify_log, b'[0-9.]+ Notify "Power" .* ".*" ".*Wireless mouse .*very low.* power.*\([0-9.]+%\).*"') + + def test_notify_device_battery_coarse_level(self): + '''critical power level notification for device batteries with coarse level''' + + # Set internal battery to discharging + self.set_composite_battery_discharging() + + # Add a device battery + bat2_path = '/org/freedesktop/UPower/devices/' + 'mock_MOUSE_BAT1' + self.obj_upower.AddObject(bat2_path, + 'org.freedesktop.UPower.Device', + { + 'PowerSupply': dbus.Boolean(False, variant_level=1), + 'IsPresent': dbus.Boolean(True, variant_level=1), + 'Model': dbus.String('Bat1', variant_level=1), + 'Percentage': dbus.Double(40.0, variant_level=1), + 'BatteryLevel': dbus.UInt32(UPowerGlib.DeviceLevel.LOW, variant_level=1), + 'TimeToEmpty': dbus.Int64(1600, variant_level=1), + 'EnergyFull': dbus.Double(100.0, variant_level=1), + 'Energy': dbus.Double(40.0, variant_level=1), + 'State': dbus.UInt32(UPowerGlib.DeviceState.DISCHARGING, variant_level=1), + 'Type': dbus.UInt32(UPowerGlib.DeviceKind.MOUSE, variant_level=1), + 'WarningLevel': dbus.UInt32(UPowerGlib.DeviceLevel.NONE, variant_level=1), + }, dbus.Array([], signature='(ssss)')) + + obj_bat2 = self.system_bus_con.get_object('org.freedesktop.UPower', bat2_path) + self.obj_upower.EmitSignal('', 'DeviceAdded', 'o', [bat2_path], + dbus_interface='org.freedesktop.DBus.Mock') + time.sleep(1) + + # now change the mouse battery to critical charge + obj_bat2.Set('org.freedesktop.UPower.Device', 'TimeToEmpty', + dbus.Int64(30, variant_level=1), + dbus_interface=dbus.PROPERTIES_IFACE) + obj_bat2.Set('org.freedesktop.UPower.Device', 'Energy', + dbus.Double(0.5, variant_level=1), + dbus_interface=dbus.PROPERTIES_IFACE) + obj_bat2.Set('org.freedesktop.UPower.Device', 'WarningLevel', + dbus.UInt32(UPowerGlib.DeviceLevel.CRITICAL, variant_level=1), + dbus_interface=dbus.PROPERTIES_IFACE) + obj_bat2.EmitSignal('', 'Changed', '', [], dbus_interface='org.freedesktop.DBus.Mock') + self.obj_upower.EmitSignal('', 'DeviceChanged', 'o', [bat2_path], + dbus_interface='org.freedesktop.DBus.Mock') + + self.check_plugin_log('EMIT: charge-critical', 2) + time.sleep(0.5) + + # we should have gotten a notification by now + notify_log = self.p_notify.stdout.read() + + # verify notification + self.assertRegex(notify_log, b'[0-9.]+ Notify "Power" .* ".*" ".*Wireless mouse .*low.* power.*"') + self.assertNotRegex(notify_log, b'[0-9.]+ Notify "Power" .* ".*" ".*\([0-9.]+%\).*"') + + def test_forced_logout(self): + '''Test forced logout''' + + self.daemon_death_expected = True + idle_delay = round(gsdpowerconstants.MINIMUM_IDLE_DIM_DELAY / gsdpowerconstants.IDLE_DELAY_TO_IDLE_DIM_MULTIPLIER) + + self.settings_session['idle-delay'] = idle_delay + self.settings_gsd_power['sleep-inactive-battery-timeout'] = idle_delay + 1 + self.settings_gsd_power['sleep-inactive-battery-type'] = 'logout' + + self.check_for_logout(idle_delay + 2) + + # The notification should have been received before the logout, but it's saved anyway + notify_log = self.p_notify.stdout.read() + self.assertTrue(b'You will soon log out because of inactivity.' in notify_log) + + def test_forced_logout_inhibition(self): + '''Test we don't force logout when inhibited''' + + idle_delay = round(gsdpowerconstants.MINIMUM_IDLE_DIM_DELAY / gsdpowerconstants.IDLE_DELAY_TO_IDLE_DIM_MULTIPLIER) + + self.settings_session['idle-delay'] = idle_delay + self.settings_gsd_power['sleep-inactive-battery-timeout'] = idle_delay + 1 + self.settings_gsd_power['sleep-inactive-battery-type'] = 'logout' + + # create suspend inhibitor which should stop us logging out + inhibit_id = self.obj_session_mgr.Inhibit( + 'testsuite', dbus.UInt32(0), 'for testing', + dbus.UInt32(gsdpowerenums.GSM_INHIBITOR_FLAG_LOGOUT), + dbus_interface='org.gnome.SessionManager') + + self.check_no_logout(idle_delay + 3) + + # Drop inhibitor + self.obj_session_mgr.Uninhibit(dbus.UInt32(inhibit_id), + dbus_interface='org.gnome.SessionManager') + +class PowerPluginTest7(PowerPluginBase): + def test_check_missing_kbd_brightness(self): + ''' https://bugzilla.gnome.org/show_bug.cgi?id=793512 ''' + + obj_gsd_power_kbd = self.session_bus_con.get_object( + 'org.gnome.SettingsDaemon.Power', '/org/gnome/SettingsDaemon/Power') + obj_gsd_power_kbd_props = dbus.Interface(obj_gsd_power_kbd, dbus.PROPERTIES_IFACE) + + # Will return -1 if gsd-power crashed, and an exception if the code caught the problem + with self.assertRaises(dbus.DBusException) as exc: + kbd_brightness = obj_gsd_power_kbd_props.Get('org.gnome.SettingsDaemon.Power.Keyboard', 'Brightness') + + # We should not have arrived here, if we did then the test failed, let's print this to help debugging + print('Got keyboard brightness: {}'.format(kbd_brightness)) + + self.assertEqual(exc.exception.get_dbus_message(), 'Failed to get property Brightness on interface org.gnome.SettingsDaemon.Power.Keyboard') + + def test_inhibitor_idletime(self): + ''' https://bugzilla.gnome.org/show_bug.cgi?id=705942 ''' + + idle_delay = round(gsdpowerconstants.MINIMUM_IDLE_DIM_DELAY / gsdpowerconstants.IDLE_DELAY_TO_IDLE_DIM_MULTIPLIER) + + self.settings_session['idle-delay'] = idle_delay + self.settings_gsd_power['sleep-inactive-battery-timeout'] = 5 + self.settings_gsd_power['sleep-inactive-battery-type'] = 'suspend' + + # create inhibitor + inhibit_id = self.obj_session_mgr.Inhibit( + 'testsuite', dbus.UInt32(0), 'for testing', + dbus.UInt32(gsdpowerenums.GSM_INHIBITOR_FLAG_IDLE), + dbus_interface='org.gnome.SessionManager') + self.check_no_suspend(idle_delay + 2) + self.check_no_dim(0) + + # Check that we didn't go to idle either + self.assertEqual(self.get_status(), gsdpowerenums.GSM_PRESENCE_STATUS_AVAILABLE) + + self.obj_session_mgr.Uninhibit(dbus.UInt32(inhibit_id), + dbus_interface='org.gnome.SessionManager') + + self.check_no_suspend(2) + self.check_no_dim(0) + + time.sleep(5) + + self.check_suspend_no_hibernate(7) + + def disabled_test_unindle_on_ac_plug(self): + idle_delay = round(gsdpowerconstants.MINIMUM_IDLE_DIM_DELAY / gsdpowerconstants.IDLE_DELAY_TO_IDLE_DIM_MULTIPLIER) + self.settings_session['idle-delay'] = idle_delay + + # Wait for idle + self.check_dim(idle_delay + 2) + + # Plug in the AC + self.obj_upower.Set('org.freedesktop.UPower', 'OnBattery', False) + self.obj_upower.EmitSignal('', 'Changed', '', [], dbus_interface='org.freedesktop.DBus.Mock') + + # Check that we undim + self.check_undim(gsdpowerconstants.POWER_UP_TIME_ON_AC / 2) + + # And wait a little more to see us dim again + self.check_dim(idle_delay + 2) + + # Unplug the AC + self.obj_upower.Set('org.freedesktop.UPower', 'OnBattery', True) + self.obj_upower.EmitSignal('', 'Changed', '', [], dbus_interface='org.freedesktop.DBus.Mock') + + # Check that we undim + self.check_undim(gsdpowerconstants.POWER_UP_TIME_ON_AC / 2) + + # And wait a little more to see us dim again + self.check_dim(idle_delay + 2) + +class PowerPluginTest8(PowerPluginBase): + def test_brightness_stepping(self): + '''Check that stepping the backlight works as expected''' + + if self.skip_sysfs_backlight: + self.skipTest("sysfs backlight support required for test") + + obj_gsd_power = self.session_bus_con.get_object( + 'org.gnome.SettingsDaemon.Power', '/org/gnome/SettingsDaemon/Power') + obj_gsd_power_screen_iface = dbus.Interface(obj_gsd_power, 'org.gnome.SettingsDaemon.Power.Screen') + + # Each of the step calls will only return when the value was written + start = time.time() + # We start at 50% and step by 5% each time + obj_gsd_power_screen_iface.StepUp() + self.assertEqual(self.get_brightness(), 55) + obj_gsd_power_screen_iface.StepUp() + self.assertEqual(self.get_brightness(), 60) + obj_gsd_power_screen_iface.StepUp() + self.assertEqual(self.get_brightness(), 65) + obj_gsd_power_screen_iface.StepUp() + self.assertEqual(self.get_brightness(), 70) + stop = time.time() + # This needs to take more than 0.8 seconds as each write is delayed by + # 0.2 seconds by the test backlight helper + self.assertGreater(stop - start, 0.8) + + # Now, the same thing should work fine if we step multiple times, + # even if we are so quick that compression will happen. + # Use a list to keep rack of replies (as integer is immutable and would + # not be modified in the outer scope) + replies = [0] + + def handle_reply(*args): + replies[0] += 1 + + def last_reply(*args): + replies[0] += 1 + loop.quit() + + def error_handler(*args): + loop.quit() + + start = time.time() + obj_gsd_power_screen_iface.StepDown(reply_handler=handle_reply, error_handler=error_handler) + obj_gsd_power_screen_iface.StepDown(reply_handler=handle_reply, error_handler=error_handler) + obj_gsd_power_screen_iface.StepDown(reply_handler=handle_reply, error_handler=error_handler) + obj_gsd_power_screen_iface.StepDown(reply_handler=last_reply, error_handler=error_handler) + loop = GLib.MainLoop() + loop.run() + stop = time.time() + + # The calls need to be returned in order. As we got the last reply, all + # others must have been received too. + self.assertEqual(replies[0], 4) + # Four steps down, so back at 50% + self.assertEqual(self.get_brightness(), 50) + # And compression must have happened, so it should take less than 0.8s + self.assertLess(stop - start, 0.8) + + def test_brightness_compression(self): + '''Check that compression also happens when setting the property''' + + if self.skip_sysfs_backlight: + self.skipTest("sysfs backlight support required for test") + + # Now test that the compression works correctly. + # NOTE: Relies on the implementation detail, that the property setter + # returns immediately rather than waiting for the brightness to + # be updated. + # Should this ever be fixed, then this will need to be changed to use + # async dbus calls similar to the stepping code + + obj_gsd_power = self.session_bus_con.get_object( + 'org.gnome.SettingsDaemon.Power', '/org/gnome/SettingsDaemon/Power') + obj_gsd_power_prop_iface = dbus.Interface(obj_gsd_power, dbus.PROPERTIES_IFACE) + + # Quickly ramp the brightness up + for brightness in range(70, 91): + obj_gsd_power_prop_iface.Set('org.gnome.SettingsDaemon.Power.Screen', 'Brightness', brightness) + + # The brightness of 80 should be in effect after slightly more than + # 0.4 seconds. If compression does not work as expected, this would take + # more than 5 seconds for the 20 steps. + time.sleep(2.0) + self.assertEqual(self.get_brightness(), 90) + + def test_brightness_uevent(self): + if self.skip_sysfs_backlight: + self.skipTest("sysfs backlight support required for test") + + obj_gsd_power = self.session_bus_con.get_object( + 'org.gnome.SettingsDaemon.Power', '/org/gnome/SettingsDaemon/Power') + obj_gsd_power_prop_iface = dbus.Interface(obj_gsd_power, dbus.PROPERTIES_IFACE) + + brightness = obj_gsd_power_prop_iface.Get('org.gnome.SettingsDaemon.Power.Screen', 'Brightness') + self.assertEqual(50, brightness) + + # Check that the brightness is updated if it was changed through some + # other mechanism (e.g. firmware). + # Set to 80+1 because of the GSD offset (see add_backlight). + self.testbed.set_attribute(self.backlight, 'brightness', '81') + self.testbed.uevent(self.backlight, 'change') + + self.check_plugin_log('GsdBacklight: Got uevent', 1, 'gsd-power did not process uevent') + time.sleep(0.2) + + brightness = obj_gsd_power_prop_iface.Get('org.gnome.SettingsDaemon.Power.Screen', 'Brightness') + self.assertEqual(80, brightness) + + def test_brightness_step(self): + if self.skip_sysfs_backlight: + self.skipTest("sysfs backlight support required for test") + + # We cannot use check_plugin_log here because the startup check already + # read the relevant message. + log = open(self.plugin_log_write.name, 'rb').read() + self.assertIn(b'Step size for backlight is 5.', log) + + def test_legacy_brightness_step(self): + if self.skip_sysfs_backlight: + self.skipTest("sysfs backlight support required for test") + + # We cannot use check_plugin_log here because the startup check already + # read the relevant message. + log = open(self.plugin_log_write.name, 'rb').read() + self.assertIn(b'Step size for backlight is 1.', log) + + def test_legacy_brightness_rounding(self): + if self.skip_sysfs_backlight: + self.skipTest("sysfs backlight support required for test") + + obj_gsd_power = self.session_bus_con.get_object( + 'org.gnome.SettingsDaemon.Power', '/org/gnome/SettingsDaemon/Power') + obj_gsd_power_prop_iface = dbus.Interface(obj_gsd_power, dbus.PROPERTIES_IFACE) + + obj_gsd_power_prop_iface.Set('org.gnome.SettingsDaemon.Power.Screen', 'Brightness', 0) + time.sleep(0.4) + self.assertEqual(self.get_brightness(), 0) + obj_gsd_power_prop_iface.Set('org.gnome.SettingsDaemon.Power.Screen', 'Brightness', 10) + time.sleep(0.4) + self.assertEqual(self.get_brightness(), 2) + obj_gsd_power_prop_iface.Set('org.gnome.SettingsDaemon.Power.Screen', 'Brightness', 20) + time.sleep(0.4) + self.assertEqual(self.get_brightness(), 3) + obj_gsd_power_prop_iface.Set('org.gnome.SettingsDaemon.Power.Screen', 'Brightness', 25) + time.sleep(0.4) + self.assertEqual(self.get_brightness(), 4) + obj_gsd_power_prop_iface.Set('org.gnome.SettingsDaemon.Power.Screen', 'Brightness', 49) + time.sleep(0.4) + self.assertEqual(self.get_brightness(), 7) + obj_gsd_power_prop_iface.Set('org.gnome.SettingsDaemon.Power.Screen', 'Brightness', 50) + time.sleep(0.4) + self.assertEqual(self.get_brightness(), 8) + obj_gsd_power_prop_iface.Set('org.gnome.SettingsDaemon.Power.Screen', 'Brightness', 56) + time.sleep(0.4) + self.assertEqual(self.get_brightness(), 8) + obj_gsd_power_prop_iface.Set('org.gnome.SettingsDaemon.Power.Screen', 'Brightness', 57) + time.sleep(0.4) + self.assertEqual(self.get_brightness(), 9) + obj_gsd_power_prop_iface.Set('org.gnome.SettingsDaemon.Power.Screen', 'Brightness', 98) + time.sleep(0.4) + self.assertEqual(self.get_brightness(), 15) + + def test_no_backlight(self): + '''Check that backlight brightness DBus api without a backlight''' + + obj_gsd_power = self.session_bus_con.get_object( + 'org.gnome.SettingsDaemon.Power', '/org/gnome/SettingsDaemon/Power') + obj_gsd_power_props = dbus.Interface(obj_gsd_power, dbus.PROPERTIES_IFACE) + obj_gsd_power_screen = dbus.Interface(obj_gsd_power, 'org.gnome.SettingsDaemon.Power.Screen') + + # We expect -1 to be returned + brightness = obj_gsd_power_props.Get('org.gnome.SettingsDaemon.Power.Screen', 'Brightness') + self.assertEqual(brightness, -1) + + # Trying to set the brightness + with self.assertRaises(dbus.DBusException) as exc: + obj_gsd_power_props.Set('org.gnome.SettingsDaemon.Power.Screen', 'Brightness', 1) + + self.assertEqual(exc.exception.get_dbus_message(), 'No usable backlight could be found!') + + with self.assertRaises(dbus.DBusException) as exc: + obj_gsd_power_screen.StepUp() + + self.assertEqual(exc.exception.get_dbus_message(), 'No usable backlight could be found!') + + with self.assertRaises(dbus.DBusException) as exc: + obj_gsd_power_screen.StepDown() + + self.assertEqual(exc.exception.get_dbus_message(), 'No usable backlight could be found!') + +# avoid writing to stderr +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/plugins/print-notifications/gsd-print-notifications-manager.c b/plugins/print-notifications/gsd-print-notifications-manager.c new file mode 100644 index 0000000..4e0b3ab --- /dev/null +++ b/plugins/print-notifications/gsd-print-notifications-manager.c @@ -0,0 +1,1725 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2011 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <sys/types.h> +#include <sys/wait.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> + +#include <locale.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> + +#include <cups/cups.h> +#include <cups/ppd.h> +#include <libnotify/notify.h> + +#include "gnome-settings-profile.h" +#include "gsd-print-notifications-manager.h" + +#define CUPS_DBUS_NAME "org.cups.cupsd.Notifier" +#define CUPS_DBUS_PATH "/org/cups/cupsd/Notifier" +#define CUPS_DBUS_INTERFACE "org.cups.cupsd.Notifier" + +#define RENEW_INTERVAL 3500 +#define SUBSCRIPTION_DURATION 3600 +#define CONNECTING_TIMEOUT 60 +#define REASON_TIMEOUT 15000 +#define CUPS_CONNECTION_TEST_INTERVAL 300 +#define CHECK_INTERVAL 60 /* secs */ +#define AUTHENTICATION_CHECK_TIMEOUT 3 + +#if (CUPS_VERSION_MAJOR > 1) || (CUPS_VERSION_MINOR > 5) +#define HAVE_CUPS_1_6 1 +#endif + +#ifndef HAVE_CUPS_1_6 +#define ippGetStatusCode(ipp) ipp->request.status.status_code +#define ippGetInteger(attr, element) attr->values[element].integer +#define ippGetString(attr, element, language) attr->values[element].string.text +#define ippGetName(attr) attr->name +#define ippGetCount(attr) attr->num_values +#define ippGetBoolean(attr, index) attr->values[index].boolean + +static ipp_attribute_t * +ippNextAttribute (ipp_t *ipp) +{ + if (!ipp || !ipp->current) + return (NULL); + return (ipp->current = ipp->current->next); +} +#endif + +struct _GsdPrintNotificationsManager +{ + GObject parent; + + GDBusConnection *cups_bus_connection; + gint subscription_id; + cups_dest_t *dests; + gint num_dests; + gboolean scp_handler_spawned; + GPid scp_handler_pid; + GList *timeouts; + GHashTable *printing_printers; + GList *active_notifications; + guint cups_connection_timeout_id; + guint check_source_id; + guint cups_dbus_subscription_id; + guint renew_source_id; + gint last_notify_sequence_number; + guint start_idle_id; + GList *held_jobs; +}; + +static void gsd_print_notifications_manager_class_init (GsdPrintNotificationsManagerClass *klass); +static void gsd_print_notifications_manager_init (GsdPrintNotificationsManager *print_notifications_manager); +static void gsd_print_notifications_manager_finalize (GObject *object); +static gboolean cups_connection_test (gpointer user_data); +static gboolean process_new_notifications (gpointer user_data); + +G_DEFINE_TYPE (GsdPrintNotificationsManager, gsd_print_notifications_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +static const char * +password_cb (const char *prompt, + http_t *http, + const char *method, + const char *resource, + void *user_data) +{ + return NULL; +} + +static char * +get_dest_attr (const char *dest_name, + const char *attr, + cups_dest_t *dests, + int num_dests) +{ + cups_dest_t *dest; + const char *value; + char *ret; + + if (dest_name == NULL) + return NULL; + + ret = NULL; + + dest = cupsGetDest (dest_name, NULL, num_dests, dests); + if (dest == NULL) { + g_debug ("Unable to find a printer named '%s'", dest_name); + goto out; + } + + value = cupsGetOption (attr, dest->num_options, dest->options); + if (value == NULL) { + g_debug ("Unable to get %s for '%s'", attr, dest_name); + goto out; + } + ret = g_strdup (value); + out: + return ret; +} + +static gboolean +is_cupsbrowsed_dest (const char *name) +{ + const char *val = NULL; + gboolean is_cupsbrowsed = FALSE; + cups_dest_t *found_dest = NULL; + + found_dest = cupsGetNamedDest (CUPS_HTTP_DEFAULT, name, NULL); + if (found_dest == NULL) { + goto out; + } + + val = cupsGetOption ("cups-browsed", found_dest->num_options, found_dest->options); + if (val == NULL) { + goto out; + } + + if (g_str_equal (val, "yes") || g_str_equal (val, "on") || g_str_equal (val, "true")) { + is_cupsbrowsed = TRUE; + } +out: + if (found_dest != NULL) { + cupsFreeDests (1, found_dest); + } + + return is_cupsbrowsed; +} + +static gboolean +is_local_dest (const char *name, + cups_dest_t *dests, + int num_dests) +{ + char *type_str; + cups_ptype_t type; + gboolean is_remote; + + is_remote = TRUE; + + type_str = get_dest_attr (name, "printer-type", dests, num_dests); + if (type_str == NULL) { + goto out; + } + + type = atoi (type_str); + is_remote = type & (CUPS_PRINTER_REMOTE | CUPS_PRINTER_IMPLICIT); + g_free (type_str); + out: + return !is_remote; +} + +static gboolean +server_is_local (const gchar *server_name) +{ + if (server_name != NULL && + (g_ascii_strncasecmp (server_name, "localhost", 9) == 0 || + g_ascii_strncasecmp (server_name, "127.0.0.1", 9) == 0 || + g_ascii_strncasecmp (server_name, "::1", 3) == 0 || + server_name[0] == '/')) { + return TRUE; + } else { + return FALSE; + } +} + +static int +strcmp0(const void *a, const void *b) +{ + return g_strcmp0 (*((gchar **) a), *((gchar **) b)); +} + +struct +{ + gchar *printer_name; + gchar *primary_text; + gchar *secondary_text; + guint timeout_id; + GsdPrintNotificationsManager *manager; +} typedef TimeoutData; + +struct +{ + gchar *printer_name; + gchar *reason; + NotifyNotification *notification; + gulong notification_close_id; + GsdPrintNotificationsManager *manager; +} typedef ReasonData; + +struct +{ + guint job_id; + gchar *printer_name; + guint timeout_id; +} typedef HeldJob; + +static void +free_timeout_data (gpointer user_data) +{ + TimeoutData *data = (TimeoutData *) user_data; + + if (data) { + g_free (data->printer_name); + g_free (data->primary_text); + g_free (data->secondary_text); + g_free (data); + } +} + +static void +free_reason_data (gpointer user_data) +{ + ReasonData *data = (ReasonData *) user_data; + + if (data) { + if (data->notification_close_id > 0 && + g_signal_handler_is_connected (data->notification, + data->notification_close_id)) + g_signal_handler_disconnect (data->notification, data->notification_close_id); + + g_object_unref (data->notification); + + g_free (data->printer_name); + g_free (data->reason); + + g_free (data); + } +} + +static void +free_held_job (gpointer user_data) +{ + HeldJob *job = (HeldJob *) user_data; + + if (job != NULL) { + g_free (job->printer_name); + g_free (job); + } +} + +static void +notification_closed_cb (NotifyNotification *notification, + gpointer user_data) +{ + ReasonData *data = (ReasonData *) user_data; + + if (data) { + data->manager->active_notifications = + g_list_remove (data->manager->active_notifications, data); + + free_reason_data (data); + } +} + +static gboolean +show_notification (gpointer user_data) +{ + NotifyNotification *notification; + TimeoutData *data = (TimeoutData *) user_data; + ReasonData *reason_data; + GList *tmp; + + if (!data) + return FALSE; + + notification = notify_notification_new (data->primary_text, + data->secondary_text, + "printer-symbolic"); + + notify_notification_set_app_name (notification, _("Printers")); + notify_notification_set_hint (notification, + "resident", + g_variant_new_boolean (TRUE)); + notify_notification_set_timeout (notification, REASON_TIMEOUT); + notify_notification_set_hint_string (notification, "desktop-entry", "gnome-printers-panel"); + + reason_data = g_new0 (ReasonData, 1); + reason_data->printer_name = g_strdup (data->printer_name); + reason_data->reason = g_strdup ("connecting-to-device"); + reason_data->notification = notification; + reason_data->manager = data->manager; + + reason_data->notification_close_id = + g_signal_connect (notification, + "closed", + G_CALLBACK (notification_closed_cb), + reason_data); + + reason_data->manager->active_notifications = + g_list_append (reason_data->manager->active_notifications, reason_data); + + notify_notification_show (notification, NULL); + + tmp = g_list_find (data->manager->timeouts, data); + if (tmp) { + data->manager->timeouts = g_list_remove_link (data->manager->timeouts, tmp); + g_list_free_full (tmp, free_timeout_data); + } + + return FALSE; +} + +static gboolean +reason_is_blacklisted (const gchar *reason) +{ + if (g_str_equal (reason, "none")) + return TRUE; + + if (g_str_equal (reason, "other")) + return TRUE; + + if (g_str_equal (reason, "com.apple.print.recoverable")) + return TRUE; + + /* https://bugzilla.redhat.com/show_bug.cgi?id=883401 */ + if (g_str_has_prefix (reason, "cups-remote-")) + return TRUE; + + /* https://bugzilla.redhat.com/show_bug.cgi?id=1207154 */ + if (g_str_equal (reason, "cups-waiting-for-job-completed")) + return TRUE; + + return FALSE; +} + +static void +on_cups_notification (GDBusConnection *connection, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer user_data) +{ + /* Ignore any signal starting with Server*. This has caused a message + * storm through ServerAudit messages in the past, see + * https://gitlab.gnome.org/GNOME/gnome-settings-daemon/issues/62 + */ + if (!signal_name || (strncmp (signal_name, "Server", 6) == 0)) + return; + + process_new_notifications (user_data); +} + +static gchar * +get_statuses_second (guint i, + const gchar *printer_name) +{ + gchar *status; + + switch (i) { + case 0: + /* Translators: The printer is low on toner (same as in system-config-printer) */ + status = g_strdup_printf (_("Printer “%s” is low on toner."), printer_name); + break; + case 1: + /* Translators: The printer has no toner left (same as in system-config-printer) */ + status = g_strdup_printf (_("Printer “%s” has no toner left."), printer_name); + break; + case 2: + /* Translators: The printer is in the process of connecting to a shared network output device (same as in system-config-printer) */ + status = g_strdup_printf (_("Printer “%s” may not be connected."), printer_name); + break; + case 3: + /* Translators: One or more covers on the printer are open (same as in system-config-printer) */ + status = g_strdup_printf (_("The cover is open on printer “%s”."), printer_name); + break; + case 4: + /* Translators: A filter or backend is not installed (same as in system-config-printer) */ + status = g_strdup_printf (_("There is a missing print filter for " + "printer “%s”."), printer_name); + break; + case 5: + /* Translators: One or more doors on the printer are open (same as in system-config-printer) */ + status = g_strdup_printf (_("The door is open on printer “%s”."), printer_name); + break; + case 6: + /* Translators: "marker" is one color bin of the printer */ + status = g_strdup_printf (_("Printer “%s” is low on a marker supply."), printer_name); + break; + case 7: + /* Translators: "marker" is one color bin of the printer */ + status = g_strdup_printf (_("Printer “%s” is out of a marker supply."), printer_name); + break; + case 8: + /* Translators: At least one input tray is low on media (same as in system-config-printer) */ + status = g_strdup_printf (_("Printer “%s” is low on paper."), printer_name); + break; + case 9: + /* Translators: At least one input tray is empty (same as in system-config-printer) */ + status = g_strdup_printf (_("Printer “%s” is out of paper."), printer_name); + break; + case 10: + /* Translators: The printer is offline (same as in system-config-printer) */ + status = g_strdup_printf (_("Printer “%s” is currently off-line."), printer_name); + break; + case 11: + /* Translators: The printer has detected an error (same as in system-config-printer) */ + status = g_strdup_printf (_("There is a problem on printer “%s”."), printer_name); + break; + default: + g_assert_not_reached (); + } + + return status; +} + +static void +authenticate_cb (NotifyNotification *notification, + gchar *action, + gpointer user_data) +{ + GAppInfo *app_info; + gboolean ret; + GError *error = NULL; + gchar *commandline; + gchar *printer_name = user_data; + + if (g_strcmp0 (action, "default") == 0) { + notify_notification_close (notification, NULL); + + commandline = g_strdup_printf (BINDIR "/gnome-control-center printers show-jobs %s", printer_name); + app_info = g_app_info_create_from_commandline (commandline, + "gnome-control-center", + G_APP_INFO_CREATE_SUPPORTS_STARTUP_NOTIFICATION, + &error); + g_free (commandline); + + if (app_info != NULL) { + ret = g_app_info_launch (app_info, + NULL, + NULL, + &error); + + if (!ret) { + g_warning ("failed to launch gnome-control-center: %s", error->message); + g_error_free (error); + } + + g_object_unref (app_info); + } else { + g_warning ("failed to create application info: %s", error->message); + g_error_free (error); + } + } +} + +static void +unref_notification (NotifyNotification *notification, + gpointer data) +{ + g_object_unref (notification); +} + +static gint +check_job_for_authentication (gpointer userdata) +{ + GsdPrintNotificationsManager *manager = userdata; + ipp_attribute_t *attr; + static gchar *requested_attributes[] = { "job-state-reasons", "job-hold-until", NULL }; + gboolean needs_authentication = FALSE; + HeldJob *job; + gchar *primary_text; + gchar *secondary_text; + gchar *job_uri; + ipp_t *request, *response; + gint i; + + if (manager->held_jobs != NULL) { + job = (HeldJob *) manager->held_jobs->data; + + manager->held_jobs = g_list_delete_link (manager->held_jobs, + manager->held_jobs); + + request = ippNewRequest (IPP_GET_JOB_ATTRIBUTES); + + job_uri = g_strdup_printf ("ipp://localhost/jobs/%u", job->job_id); + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_URI, + "job-uri", NULL, job_uri); + g_free (job_uri); + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_NAME, + "requesting-user-name", NULL, cupsUser ()); + ippAddStrings (request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, + "requested-attributes", 2, NULL, (const char **) requested_attributes); + + response = cupsDoRequest (CUPS_HTTP_DEFAULT, request, "/"); + if (response != NULL) { + if (ippGetStatusCode (response) <= IPP_OK_CONFLICT) { + if ((attr = ippFindAttribute (response, "job-state-reasons", IPP_TAG_ZERO)) != NULL) { + for (i = 0; i < ippGetCount (attr); i++) { + if (g_strcmp0 (ippGetString (attr, i, NULL), "cups-held-for-authentication") == 0) { + needs_authentication = TRUE; + break; + } + } + } + + if (!needs_authentication && (attr = ippFindAttribute (response, "job-hold-until", IPP_TAG_ZERO)) != NULL) { + if (g_strcmp0 (ippGetString (attr, 0, NULL), "auth-info-required") == 0) + needs_authentication = TRUE; + } + } + + ippDelete (response); + } + + if (needs_authentication) { + NotifyNotification *notification; + + /* Translators: The printer has a job to print but the printer needs authentication to continue with the print */ + primary_text = g_strdup_printf (_("%s Requires Authentication"), job->printer_name); + /* Translators: A printer needs credentials to continue printing a job */ + secondary_text = g_strdup_printf (_("Credentials required in order to print")); + + notification = notify_notification_new (primary_text, + secondary_text, + "printer-symbolic"); + notify_notification_set_app_name (notification, _("Printers")); + notify_notification_set_hint_string (notification, "desktop-entry", "gnome-printers-panel"); + notify_notification_add_action (notification, + "default", + /* This is a default action so the label won't be shown */ + "Authenticate", + authenticate_cb, + g_strdup (job->printer_name), g_free); + g_signal_connect (notification, "closed", G_CALLBACK (unref_notification), NULL); + + notify_notification_show (notification, NULL); + + g_free (primary_text); + g_free (secondary_text); + } + + free_held_job (job); + } + + return G_SOURCE_REMOVE; +} + +static void +process_cups_notification (GsdPrintNotificationsManager *manager, + const char *notify_subscribed_event, + const char *notify_text, + const char *notify_printer_uri, + const char *printer_name, + gint printer_state, + const char *printer_state_reasons, + gboolean printer_is_accepting_jobs, + guint notify_job_id, + gint job_state, + const char *job_state_reasons, + const char *job_name, + gint job_impressions_completed) +{ + ipp_attribute_t *attr; + gboolean my_job = FALSE; + gboolean known_reason; + HeldJob *held_job; + http_t *http; + gchar *primary_text = NULL; + gchar *secondary_text = NULL; + gchar *job_uri = NULL; + ipp_t *request, *response; + static const char * const reasons[] = { + "toner-low", + "toner-empty", + "connecting-to-device", + "cover-open", + "cups-missing-filter", + "door-open", + "marker-supply-low", + "marker-supply-empty", + "media-low", + "media-empty", + "offline", + "other"}; + + static const char * statuses_first[] = { + /* Translators: The printer is low on toner (same as in system-config-printer) */ + N_("Toner low"), + /* Translators: The printer has no toner left (same as in system-config-printer) */ + N_("Toner empty"), + /* Translators: The printer is in the process of connecting to a shared network output device (same as in system-config-printer) */ + N_("Not connected?"), + /* Translators: One or more covers on the printer are open (same as in system-config-printer) */ + N_("Cover open"), + /* Translators: A filter or backend is not installed (same as in system-config-printer) */ + N_("Printer configuration error"), + /* Translators: One or more doors on the printer are open (same as in system-config-printer) */ + N_("Door open"), + /* Translators: "marker" is one color bin of the printer */ + N_("Marker supply low"), + /* Translators: "marker" is one color bin of the printer */ + N_("Out of a marker supply"), + /* Translators: At least one input tray is low on media (same as in system-config-printer) */ + N_("Paper low"), + /* Translators: At least one input tray is empty (same as in system-config-printer) */ + N_("Out of paper"), + /* Translators: The printer is offline (same as in system-config-printer) */ + N_("Printer off-line"), + /* Translators: The printer has detected an error (same as in system-config-printer) */ + N_("Printer error") }; + + if (g_strcmp0 (notify_subscribed_event, "printer-added") != 0 && + g_strcmp0 (notify_subscribed_event, "printer-deleted") != 0 && + g_strcmp0 (notify_subscribed_event, "printer-state-changed") != 0 && + g_strcmp0 (notify_subscribed_event, "job-completed") != 0 && + g_strcmp0 (notify_subscribed_event, "job-state-changed") != 0 && + g_strcmp0 (notify_subscribed_event, "job-created") != 0) + return; + + if (notify_job_id > 0) { + if ((http = httpConnectEncrypt (cupsServer (), ippPort (), + cupsEncryption ())) == NULL) { + g_debug ("Connection to CUPS server \'%s\' failed.", cupsServer ()); + } else { + job_uri = g_strdup_printf ("ipp://localhost/jobs/%d", notify_job_id); + + request = ippNewRequest (IPP_GET_JOB_ATTRIBUTES); + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_URI, + "job-uri", NULL, job_uri); + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_NAME, + "requesting-user-name", NULL, cupsUser ()); + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, + "requested-attributes", NULL, "job-originating-user-name"); + response = cupsDoRequest (http, request, "/"); + + if (response) { + if (ippGetStatusCode (response) <= IPP_OK_CONFLICT && + (attr = ippFindAttribute(response, "job-originating-user-name", + IPP_TAG_NAME))) { + if (g_strcmp0 (ippGetString (attr, 0, NULL), cupsUser ()) == 0) + my_job = TRUE; + } + ippDelete(response); + } + g_free (job_uri); + httpClose (http); + } + } + + if (g_strcmp0 (notify_subscribed_event, "printer-added") == 0) { + cupsFreeDests (manager->num_dests, manager->dests); + manager->num_dests = cupsGetDests (&manager->dests); + + if (is_local_dest (printer_name, + manager->dests, + manager->num_dests) && + !is_cupsbrowsed_dest (printer_name)) { + /* Translators: New printer has been added */ + primary_text = g_strdup (_("Printer added")); + secondary_text = g_strdup (printer_name); + } + } else if (g_strcmp0 (notify_subscribed_event, "printer-deleted") == 0) { + cupsFreeDests (manager->num_dests, manager->dests); + manager->num_dests = cupsGetDests (&manager->dests); + } else if (g_strcmp0 (notify_subscribed_event, "job-completed") == 0 && my_job) { + g_hash_table_remove (manager->printing_printers, + printer_name); + + switch (job_state) { + case IPP_JOB_PENDING: + case IPP_JOB_HELD: + case IPP_JOB_PROCESSING: + break; + case IPP_JOB_STOPPED: + /* Translators: A print job has been stopped */ + primary_text = g_strdup (C_("print job state", "Printing stopped")); + /* Translators: "print-job xy" on a printer */ + secondary_text = g_strdup_printf (C_("print job", "“%s” on %s"), job_name, printer_name); + break; + case IPP_JOB_CANCELED: + /* Translators: A print job has been canceled */ + primary_text = g_strdup (C_("print job state", "Printing canceled")); + /* Translators: "print-job xy" on a printer */ + secondary_text = g_strdup_printf (C_("print job", "“%s” on %s"), job_name, printer_name); + break; + case IPP_JOB_ABORTED: + /* Translators: A print job has been aborted */ + primary_text = g_strdup (C_("print job state", "Printing aborted")); + /* Translators: "print-job xy" on a printer */ + secondary_text = g_strdup_printf (C_("print job", "“%s” on %s"), job_name, printer_name); + break; + case IPP_JOB_COMPLETED: + /* Translators: A print job has been completed */ + primary_text = g_strdup (C_("print job state", "Printing completed")); + /* Translators: "print-job xy" on a printer */ + secondary_text = g_strdup_printf (C_("print job", "“%s” on %s"), job_name, printer_name); + break; + } + } else if (g_strcmp0 (notify_subscribed_event, "job-state-changed") == 0 && my_job) { + switch (job_state) { + case IPP_JOB_PROCESSING: + g_hash_table_insert (manager->printing_printers, + g_strdup (printer_name), NULL); + + /* Translators: A job is printing */ + primary_text = g_strdup (C_("print job state", "Printing")); + /* Translators: "print-job xy" on a printer */ + secondary_text = g_strdup_printf (C_("print job", "“%s” on %s"), job_name, printer_name); + break; + case IPP_JOB_STOPPED: + g_hash_table_remove (manager->printing_printers, + printer_name); + /* Translators: A print job has been stopped */ + primary_text = g_strdup (C_("print job state", "Printing stopped")); + /* Translators: "print-job xy" on a printer */ + secondary_text = g_strdup_printf (C_("print job", "“%s” on %s"), job_name, printer_name); + break; + case IPP_JOB_CANCELED: + g_hash_table_remove (manager->printing_printers, + printer_name); + /* Translators: A print job has been canceled */ + primary_text = g_strdup (C_("print job state", "Printing canceled")); + /* Translators: "print-job xy" on a printer */ + secondary_text = g_strdup_printf (C_("print job", "“%s” on %s"), job_name, printer_name); + break; + case IPP_JOB_ABORTED: + g_hash_table_remove (manager->printing_printers, + printer_name); + /* Translators: A print job has been aborted */ + primary_text = g_strdup (C_("print job state", "Printing aborted")); + /* Translators: "print-job xy" on a printer */ + secondary_text = g_strdup_printf (C_("print job", "“%s” on %s"), job_name, printer_name); + break; + case IPP_JOB_COMPLETED: + g_hash_table_remove (manager->printing_printers, + printer_name); + /* Translators: A print job has been completed */ + primary_text = g_strdup (C_("print job state", "Printing completed")); + /* Translators: "print-job xy" on a printer */ + secondary_text = g_strdup_printf (C_("print job", "“%s” on %s"), job_name, printer_name); + break; + case IPP_JOB_HELD: + held_job = g_new (HeldJob, 1); + held_job->job_id = notify_job_id; + held_job->printer_name = g_strdup (printer_name); + /* CUPS takes sometime to change the "job-state-reasons" to "cups-held-for-authentication" + after the job changes job-state to "held" state but this change is not signalized + by any event so we just check the job-state-reason (or job-hold-until) after some timeout */ + held_job->timeout_id = g_timeout_add_seconds (AUTHENTICATION_CHECK_TIMEOUT, check_job_for_authentication, manager); + + manager->held_jobs = g_list_append (manager->held_jobs, held_job); + break; + default: + break; + } + } else if (g_strcmp0 (notify_subscribed_event, "job-created") == 0 && my_job) { + if (job_state == IPP_JOB_PROCESSING) { + g_hash_table_insert (manager->printing_printers, + g_strdup (printer_name), NULL); + + /* Translators: A job is printing */ + primary_text = g_strdup (C_("print job state", "Printing")); + /* Translators: "print-job xy" on a printer */ + secondary_text = g_strdup_printf (C_("print job", "“%s” on %s"), job_name, printer_name); + } + } else if (g_strcmp0 (notify_subscribed_event, "printer-state-changed") == 0) { + cups_dest_t *dest = NULL; + const gchar *tmp_printer_state_reasons = NULL; + GSList *added_reasons = NULL; + GSList *tmp_list = NULL; + GList *tmp; + gchar **old_state_reasons = NULL; + gchar **new_state_reasons = NULL; + gint i, j; + + /* Remove timeout which shows notification about possible disconnection of printer + * if "connecting-to-device" has vanished. + */ + if (printer_state_reasons == NULL || + g_strrstr (printer_state_reasons, "connecting-to-device") == NULL) { + TimeoutData *data; + + for (tmp = manager->timeouts; tmp; tmp = g_list_next (tmp)) { + data = (TimeoutData *) tmp->data; + if (g_strcmp0 (printer_name, data->printer_name) == 0) { + g_source_remove (data->timeout_id); + manager->timeouts = g_list_remove_link (manager->timeouts, tmp); + g_list_free_full (tmp, free_timeout_data); + break; + } + } + } + + for (tmp = manager->active_notifications; tmp; tmp = g_list_next (tmp)) { + ReasonData *reason_data = (ReasonData *) tmp->data; + GList *remove_list; + + if (printer_state_reasons == NULL || + (g_strcmp0 (printer_name, reason_data->printer_name) == 0 && + g_strrstr (printer_state_reasons, reason_data->reason) == NULL)) { + + if (reason_data->notification_close_id > 0 && + g_signal_handler_is_connected (reason_data->notification, + reason_data->notification_close_id)) { + g_signal_handler_disconnect (reason_data->notification, + reason_data->notification_close_id); + reason_data->notification_close_id = 0; + } + + notify_notification_close (reason_data->notification, NULL); + + remove_list = tmp; + tmp = g_list_next (tmp); + manager->active_notifications = + g_list_remove_link (manager->active_notifications, remove_list); + + g_list_free_full (remove_list, free_reason_data); + } + } + + /* Check whether we are printing on this printer right now. */ + if (g_hash_table_lookup_extended (manager->printing_printers, printer_name, NULL, NULL)) { + dest = cupsGetDest (printer_name, + NULL, + manager->num_dests, + manager->dests); + if (dest) + tmp_printer_state_reasons = cupsGetOption ("printer-state-reasons", + dest->num_options, + dest->options); + + if (tmp_printer_state_reasons) + old_state_reasons = g_strsplit (tmp_printer_state_reasons, ",", -1); + + cupsFreeDests (manager->num_dests, manager->dests); + manager->num_dests = cupsGetDests (&manager->dests); + + dest = cupsGetDest (printer_name, + NULL, + manager->num_dests, + manager->dests); + if (dest) + tmp_printer_state_reasons = cupsGetOption ("printer-state-reasons", + dest->num_options, + dest->options); + + if (tmp_printer_state_reasons) + new_state_reasons = g_strsplit (tmp_printer_state_reasons, ",", -1); + + if (new_state_reasons) + qsort (new_state_reasons, + g_strv_length (new_state_reasons), + sizeof (gchar *), + strcmp0); + + if (old_state_reasons) { + qsort (old_state_reasons, + g_strv_length (old_state_reasons), + sizeof (gchar *), + strcmp0); + + j = 0; + for (i = 0; new_state_reasons && i < g_strv_length (new_state_reasons); i++) { + while (old_state_reasons[j] && + g_strcmp0 (old_state_reasons[j], new_state_reasons[i]) < 0) + j++; + + if (old_state_reasons[j] == NULL || + g_strcmp0 (old_state_reasons[j], new_state_reasons[i]) != 0) + added_reasons = g_slist_append (added_reasons, + new_state_reasons[i]); + } + } else { + for (i = 0; new_state_reasons && i < g_strv_length (new_state_reasons); i++) { + added_reasons = g_slist_append (added_reasons, + new_state_reasons[i]); + } + } + + for (tmp_list = added_reasons; tmp_list; tmp_list = tmp_list->next) { + gchar *data = (gchar *) tmp_list->data; + known_reason = FALSE; + for (j = 0; j < G_N_ELEMENTS (reasons); j++) { + if (strncmp (data, + reasons[j], + strlen (reasons[j])) == 0) { + NotifyNotification *notification; + known_reason = TRUE; + + if (g_strcmp0 (reasons[j], "connecting-to-device") == 0) { + TimeoutData *data; + + data = g_new0 (TimeoutData, 1); + data->printer_name = g_strdup (printer_name); + data->primary_text = g_strdup ( _(statuses_first[j])); + data->secondary_text = get_statuses_second (j, printer_name); + data->manager = manager; + + data->timeout_id = g_timeout_add_seconds (CONNECTING_TIMEOUT, show_notification, data); + g_source_set_name_by_id (data->timeout_id, "[gnome-settings-daemon] show_notification"); + manager->timeouts = g_list_append (manager->timeouts, data); + } else { + ReasonData *reason_data; + gchar *second_row = get_statuses_second (j, printer_name); + + notification = notify_notification_new ( _(statuses_first[j]), + second_row, + "printer-symbolic"); + notify_notification_set_app_name (notification, _("Printers")); + notify_notification_set_hint_string (notification, "desktop-entry", "gnome-printers-panel"); + notify_notification_set_hint (notification, + "resident", + g_variant_new_boolean (TRUE)); + notify_notification_set_timeout (notification, REASON_TIMEOUT); + + reason_data = g_new0 (ReasonData, 1); + reason_data->printer_name = g_strdup (printer_name); + reason_data->reason = g_strdup (reasons[j]); + reason_data->notification = notification; + reason_data->manager = manager; + + reason_data->notification_close_id = + g_signal_connect (notification, + "closed", + G_CALLBACK (notification_closed_cb), + reason_data); + + manager->active_notifications = + g_list_append (manager->active_notifications, reason_data); + + notify_notification_show (notification, NULL); + + g_free (second_row); + } + } + } + + if (!known_reason && + !reason_is_blacklisted (data)) { + NotifyNotification *notification; + ReasonData *reason_data; + gchar *first_row; + gchar *second_row; + gchar *text = NULL; + gchar *ppd_file_name; + ppd_file_t *ppd_file; + char buffer[8192]; + + ppd_file_name = g_strdup (cupsGetPPD (printer_name)); + if (ppd_file_name) { + ppd_file = ppdOpenFile (ppd_file_name); + if (ppd_file) { + gchar **tmpv; + static const char * const schemes[] = { + "text", "http", "help", "file" + }; + + tmpv = g_new0 (gchar *, G_N_ELEMENTS (schemes) + 1); + i = 0; + for (j = 0; j < G_N_ELEMENTS (schemes); j++) { + if (ppdLocalizeIPPReason (ppd_file, data, schemes[j], buffer, sizeof (buffer))) { + tmpv[i++] = g_strdup (buffer); + } + } + + if (i > 0) + text = g_strjoinv (", ", tmpv); + g_strfreev (tmpv); + + ppdClose (ppd_file); + } + + g_unlink (ppd_file_name); + g_free (ppd_file_name); + } + + + if (g_str_has_suffix (data, "-report")) + /* Translators: This is a title of a report notification for a printer */ + first_row = g_strdup (_("Printer report")); + else if (g_str_has_suffix (data, "-warning")) + /* Translators: This is a title of a warning notification for a printer */ + first_row = g_strdup (_("Printer warning")); + else + /* Translators: This is a title of an error notification for a printer */ + first_row = g_strdup (_("Printer error")); + + + if (text == NULL) + text = g_strdup (data); + + /* Translators: "Printer 'MyPrinterName': 'Description of the report/warning/error from a PPD file'." */ + second_row = g_strdup_printf (_("Printer “%s”: “%s”."), printer_name, text); + g_free (text); + + + notification = notify_notification_new (first_row, + second_row, + "printer-symbolic"); + notify_notification_set_app_name (notification, _("Printers")); + notify_notification_set_hint_string (notification, "desktop-entry", "gnome-printers-panel"); + notify_notification_set_hint (notification, + "resident", + g_variant_new_boolean (TRUE)); + notify_notification_set_timeout (notification, REASON_TIMEOUT); + + reason_data = g_new0 (ReasonData, 1); + reason_data->printer_name = g_strdup (printer_name); + reason_data->reason = g_strdup (data); + reason_data->notification = notification; + reason_data->manager = manager; + + reason_data->notification_close_id = + g_signal_connect (notification, + "closed", + G_CALLBACK (notification_closed_cb), + reason_data); + + manager->active_notifications = + g_list_append (manager->active_notifications, reason_data); + + notify_notification_show (notification, NULL); + + g_free (first_row); + g_free (second_row); + } + } + g_slist_free (added_reasons); + } + + if (new_state_reasons) + g_strfreev (new_state_reasons); + + if (old_state_reasons) + g_strfreev (old_state_reasons); + } + + + if (primary_text) { + NotifyNotification *notification; + notification = notify_notification_new (primary_text, + secondary_text, + "printer-symbolic"); + notify_notification_set_app_name (notification, _("Printers")); + notify_notification_set_hint_string (notification, "desktop-entry", "gnome-printers-panel"); + notify_notification_set_hint (notification, "transient", g_variant_new_boolean (TRUE)); + notify_notification_show (notification, NULL); + g_object_unref (notification); + g_free (primary_text); + g_free (secondary_text); + } +} + +static gboolean +process_new_notifications (gpointer user_data) +{ + GsdPrintNotificationsManager *manager = (GsdPrintNotificationsManager *) user_data; + ipp_attribute_t *attr; + const gchar *notify_subscribed_event = NULL; + const gchar *printer_name = NULL; + const gchar *notify_text = NULL; + const gchar *notify_printer_uri = NULL; + gchar *job_state_reasons = NULL; + const gchar *job_name = NULL; + const char *attr_name; + gboolean printer_is_accepting_jobs = FALSE; + gchar *printer_state_reasons = NULL; + gchar **reasons; + guint notify_job_id = 0; + ipp_t *request; + ipp_t *response; + gint printer_state = -1; + gint job_state = -1; + gint job_impressions_completed = -1; + gint notify_sequence_number = -1; + gint i; + + request = ippNewRequest (IPP_GET_NOTIFICATIONS); + + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_NAME, + "requesting-user-name", NULL, cupsUser ()); + + ippAddInteger (request, IPP_TAG_OPERATION, IPP_TAG_INTEGER, + "notify-subscription-ids", manager->subscription_id); + + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, + "/printers/"); + + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_URI, "job-uri", NULL, + "/jobs/"); + + ippAddInteger (request, IPP_TAG_OPERATION, IPP_TAG_INTEGER, + "notify-sequence-numbers", + manager->last_notify_sequence_number + 1); + + + response = cupsDoRequest (CUPS_HTTP_DEFAULT, request, "/"); + + + for (attr = ippFindAttribute (response, "notify-sequence-number", IPP_TAG_INTEGER); + attr != NULL; + attr = ippNextAttribute (response)) { + + attr_name = ippGetName (attr); + if (g_strcmp0 (attr_name, "notify-sequence-number") == 0) { + notify_sequence_number = ippGetInteger (attr, 0); + + if (notify_sequence_number > manager->last_notify_sequence_number) + manager->last_notify_sequence_number = notify_sequence_number; + + if (notify_subscribed_event != NULL) { + process_cups_notification (manager, + notify_subscribed_event, + notify_text, + notify_printer_uri, + printer_name, + printer_state, + printer_state_reasons, + printer_is_accepting_jobs, + notify_job_id, + job_state, + job_state_reasons, + job_name, + job_impressions_completed); + + g_clear_pointer (&printer_state_reasons, g_free); + g_clear_pointer (&job_state_reasons, g_free); + } + + notify_subscribed_event = NULL; + notify_text = NULL; + notify_printer_uri = NULL; + printer_name = NULL; + printer_state = -1; + printer_state_reasons = NULL; + printer_is_accepting_jobs = FALSE; + notify_job_id = 0; + job_state = -1; + job_state_reasons = NULL; + job_name = NULL; + job_impressions_completed = -1; + } else if (g_strcmp0 (attr_name, "notify-subscribed-event") == 0) { + notify_subscribed_event = ippGetString (attr, 0, NULL); + } else if (g_strcmp0 (attr_name, "notify-text") == 0) { + notify_text = ippGetString (attr, 0, NULL); + } else if (g_strcmp0 (attr_name, "notify-printer-uri") == 0) { + notify_printer_uri = ippGetString (attr, 0, NULL); + } else if (g_strcmp0 (attr_name, "printer-name") == 0) { + printer_name = ippGetString (attr, 0, NULL); + } else if (g_strcmp0 (attr_name, "printer-state") == 0) { + printer_state = ippGetInteger (attr, 0); + } else if (g_strcmp0 (attr_name, "printer-state-reasons") == 0) { + reasons = g_new0 (gchar *, ippGetCount (attr) + 1); + for (i = 0; i < ippGetCount (attr); i++) + reasons[i] = g_strdup (ippGetString (attr, i, NULL)); + printer_state_reasons = g_strjoinv (",", reasons); + g_strfreev (reasons); + } else if (g_strcmp0 (attr_name, "printer-is-accepting-jobs") == 0) { + printer_is_accepting_jobs = ippGetBoolean (attr, 0); + } else if (g_strcmp0 (attr_name, "notify-job-id") == 0) { + notify_job_id = ippGetInteger (attr, 0); + } else if (g_strcmp0 (attr_name, "job-state") == 0) { + job_state = ippGetInteger (attr, 0); + } else if (g_strcmp0 (attr_name, "job-state-reasons") == 0) { + reasons = g_new0 (gchar *, ippGetCount (attr) + 1); + for (i = 0; i < ippGetCount (attr); i++) + reasons[i] = g_strdup (ippGetString (attr, i, NULL)); + job_state_reasons = g_strjoinv (",", reasons); + g_strfreev (reasons); + } else if (g_strcmp0 (attr_name, "job-name") == 0) { + job_name = ippGetString (attr, 0, NULL); + } else if (g_strcmp0 (attr_name, "job-impressions-completed") == 0) { + job_impressions_completed = ippGetInteger (attr, 0); + } + } + + if (notify_subscribed_event != NULL) { + process_cups_notification (manager, + notify_subscribed_event, + notify_text, + notify_printer_uri, + printer_name, + printer_state, + printer_state_reasons, + printer_is_accepting_jobs, + notify_job_id, + job_state, + job_state_reasons, + job_name, + job_impressions_completed); + + g_clear_pointer (&printer_state_reasons, g_free); + g_clear_pointer (&job_state_reasons, g_free); + } + + if (response != NULL) + ippDelete (response); + + return TRUE; +} + +static void +scp_handler (GsdPrintNotificationsManager *manager, + gboolean start) +{ + if (start) { + GError *error = NULL; + char *args[2]; + + if (manager->scp_handler_spawned) + return; + + args[0] = LIBEXECDIR "/gsd-printer"; + args[1] = NULL; + + g_spawn_async (NULL, args, NULL, + 0, NULL, NULL, + &manager->scp_handler_pid, &error); + + manager->scp_handler_spawned = (error == NULL); + + if (error) { + g_warning ("Could not execute system-config-printer-udev handler: %s", + error->message); + g_error_free (error); + } + } else if (manager->scp_handler_spawned) { + kill (manager->scp_handler_pid, SIGHUP); + g_spawn_close_pid (manager->scp_handler_pid); + manager->scp_handler_spawned = FALSE; + } +} + +static void +cancel_subscription (gint id) +{ + http_t *http; + ipp_t *request; + + if (id >= 0 && + ((http = httpConnectEncrypt (cupsServer (), ippPort (), + cupsEncryption ())) != NULL)) { + request = ippNewRequest (IPP_CANCEL_SUBSCRIPTION); + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_URI, + "printer-uri", NULL, "/"); + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_NAME, + "requesting-user-name", NULL, cupsUser ()); + ippAddInteger (request, IPP_TAG_OPERATION, IPP_TAG_INTEGER, + "notify-subscription-id", id); + ippDelete (cupsDoRequest (http, request, "/")); + httpClose (http); + } +} + +static gboolean +renew_subscription (gpointer data) +{ + GsdPrintNotificationsManager *manager = (GsdPrintNotificationsManager *) data; + ipp_attribute_t *attr = NULL; + http_t *http; + ipp_t *request; + ipp_t *response; + gint num_events = 7; + static const char * const events[] = { + "job-created", + "job-completed", + "job-state-changed", + "job-state", + "printer-added", + "printer-deleted", + "printer-state-changed"}; + + if ((http = httpConnectEncrypt (cupsServer (), ippPort (), + cupsEncryption ())) == NULL) { + g_debug ("Connection to CUPS server \'%s\' failed.", cupsServer ()); + } else { + if (manager->subscription_id >= 0) { + request = ippNewRequest (IPP_RENEW_SUBSCRIPTION); + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_URI, + "printer-uri", NULL, "/"); + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_NAME, + "requesting-user-name", NULL, cupsUser ()); + ippAddInteger (request, IPP_TAG_OPERATION, IPP_TAG_INTEGER, + "notify-subscription-id", manager->subscription_id); + ippAddInteger (request, IPP_TAG_SUBSCRIPTION, IPP_TAG_INTEGER, + "notify-lease-duration", SUBSCRIPTION_DURATION); + ippDelete (cupsDoRequest (http, request, "/")); + } else { + request = ippNewRequest (IPP_CREATE_PRINTER_SUBSCRIPTION); + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_URI, + "printer-uri", NULL, + "/"); + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_NAME, + "requesting-user-name", NULL, cupsUser ()); + ippAddStrings (request, IPP_TAG_SUBSCRIPTION, IPP_TAG_KEYWORD, + "notify-events", num_events, NULL, events); + ippAddString (request, IPP_TAG_SUBSCRIPTION, IPP_TAG_KEYWORD, + "notify-pull-method", NULL, "ippget"); + if (server_is_local (cupsServer ())) { + ippAddString (request, IPP_TAG_SUBSCRIPTION, IPP_TAG_URI, + "notify-recipient-uri", NULL, "dbus://"); + } + ippAddInteger (request, IPP_TAG_SUBSCRIPTION, IPP_TAG_INTEGER, + "notify-lease-duration", SUBSCRIPTION_DURATION); + response = cupsDoRequest (http, request, "/"); + + if (response != NULL && ippGetStatusCode (response) <= IPP_OK_CONFLICT) { + if ((attr = ippFindAttribute (response, "notify-subscription-id", + IPP_TAG_INTEGER)) == NULL) + g_debug ("No notify-subscription-id in response!\n"); + else + manager->subscription_id = ippGetInteger (attr, 0); + } + + if (response) + ippDelete (response); + } + httpClose (http); + } + return TRUE; +} + +static void +renew_subscription_with_connection_test_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GSocketConnection *connection; + GError *error = NULL; + + connection = g_socket_client_connect_to_host_finish (G_SOCKET_CLIENT (source_object), + res, + &error); + + if (connection) { + g_debug ("Test connection to CUPS server \'%s:%d\' succeeded.", cupsServer (), ippPort ()); + + g_io_stream_close (G_IO_STREAM (connection), NULL, NULL); + g_object_unref (connection); + + renew_subscription (user_data); + } else { + g_debug ("Test connection to CUPS server \'%s:%d\' failed.", cupsServer (), ippPort ()); + } +} + +static gboolean +renew_subscription_with_connection_test (gpointer user_data) +{ + GSocketClient *client; + gchar *address; + int port; + + port = ippPort (); + + address = g_strdup_printf ("%s:%d", cupsServer (), port); + + if (address && address[0] != '/') { + client = g_socket_client_new (); + + g_debug ("Initiating test connection to CUPS server \'%s:%d\'.", cupsServer (), port); + + g_socket_client_connect_to_host_async (client, + address, + port, + NULL, + renew_subscription_with_connection_test_cb, + user_data); + + g_object_unref (client); + } else { + renew_subscription (user_data); + } + + g_free (address); + + return TRUE; +} + +static void +renew_subscription_timeout_enable (GsdPrintNotificationsManager *manager, + gboolean enable, + gboolean with_connection_test) +{ + if (manager->renew_source_id > 0) + g_source_remove (manager->renew_source_id); + + if (enable) { + renew_subscription (manager); + if (with_connection_test) { + manager->renew_source_id = + g_timeout_add_seconds (RENEW_INTERVAL, + renew_subscription_with_connection_test, + manager); + g_source_set_name_by_id (manager->renew_source_id, "[gnome-settings-daemon] renew_subscription_with_connection_test"); + } else { + manager->renew_source_id = + g_timeout_add_seconds (RENEW_INTERVAL, + renew_subscription, + manager); + g_source_set_name_by_id (manager->renew_source_id, "[gnome-settings-daemon] renew_subscription"); + } + } else { + manager->renew_source_id = 0; + } +} + +static void +cups_connection_test_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsdPrintNotificationsManager *manager = (GsdPrintNotificationsManager *) user_data; + GSocketConnection *connection; + GError *error = NULL; + + connection = g_socket_client_connect_to_host_finish (G_SOCKET_CLIENT (source_object), + res, + &error); + + if (connection) { + g_debug ("Test connection to CUPS server \'%s:%d\' succeeded.", cupsServer (), ippPort ()); + + g_io_stream_close (G_IO_STREAM (connection), NULL, NULL); + g_object_unref (connection); + + manager->num_dests = cupsGetDests (&manager->dests); + g_debug ("Got dests from remote CUPS server."); + + renew_subscription_timeout_enable (manager, TRUE, TRUE); + manager->check_source_id = g_timeout_add_seconds (CHECK_INTERVAL, process_new_notifications, manager); + g_source_set_name_by_id (manager->check_source_id, "[gnome-settings-daemon] process_new_notifications"); + } else { + g_debug ("Test connection to CUPS server \'%s:%d\' failed.", cupsServer (), ippPort ()); + if (manager->cups_connection_timeout_id == 0) { + manager->cups_connection_timeout_id = + g_timeout_add_seconds (CUPS_CONNECTION_TEST_INTERVAL, cups_connection_test, manager); + g_source_set_name_by_id (manager->cups_connection_timeout_id, "[gnome-settings-daemon] cups_connection_test"); + } + } +} + +static gboolean +cups_connection_test (gpointer user_data) +{ + GsdPrintNotificationsManager *manager = (GsdPrintNotificationsManager *) user_data; + GSocketClient *client; + gchar *address; + int port = ippPort (); + + if (!manager->dests) { + address = g_strdup_printf ("%s:%d", cupsServer (), port); + + client = g_socket_client_new (); + + g_debug ("Initiating test connection to CUPS server \'%s:%d\'.", cupsServer (), port); + + g_socket_client_connect_to_host_async (client, + address, + port, + NULL, + cups_connection_test_cb, + manager); + + g_object_unref (client); + g_free (address); + } + + if (manager->dests) { + manager->cups_connection_timeout_id = 0; + + return FALSE; + } else { + return TRUE; + } +} + +static void +gsd_print_notifications_manager_got_dbus_connection (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsdPrintNotificationsManager *manager = (GsdPrintNotificationsManager *) user_data; + GError *error = NULL; + + manager->cups_bus_connection = g_bus_get_finish (res, &error); + + if (manager->cups_bus_connection != NULL) { + manager->cups_dbus_subscription_id = + g_dbus_connection_signal_subscribe (manager->cups_bus_connection, + NULL, + CUPS_DBUS_INTERFACE, + NULL, + CUPS_DBUS_PATH, + NULL, + 0, + on_cups_notification, + manager, + NULL); + } else { + g_warning ("Connection to message bus failed: %s", error->message); + g_error_free (error); + } +} + +static gboolean +gsd_print_notifications_manager_start_idle (gpointer data) +{ + GsdPrintNotificationsManager *manager = data; + + gnome_settings_profile_start (NULL); + + manager->printing_printers = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + /* + * Set a password callback which cancels authentication + * before we prepare a correct solution (see bug #725440). + */ + cupsSetPasswordCB2 (password_cb, NULL); + + if (server_is_local (cupsServer ())) { + manager->num_dests = cupsGetDests (&manager->dests); + g_debug ("Got dests from local CUPS server."); + + renew_subscription_timeout_enable (manager, TRUE, FALSE); + + g_bus_get (G_BUS_TYPE_SYSTEM, + NULL, + gsd_print_notifications_manager_got_dbus_connection, + data); + } else { + cups_connection_test (manager); + } + + scp_handler (manager, TRUE); + + gnome_settings_profile_end (NULL); + + manager->start_idle_id = 0; + return G_SOURCE_REMOVE; +} + +gboolean +gsd_print_notifications_manager_start (GsdPrintNotificationsManager *manager, + GError **error) +{ + g_debug ("Starting print-notifications manager"); + + gnome_settings_profile_start (NULL); + + manager->subscription_id = -1; + manager->dests = NULL; + manager->num_dests = 0; + manager->scp_handler_spawned = FALSE; + manager->timeouts = NULL; + manager->printing_printers = NULL; + manager->active_notifications = NULL; + manager->cups_bus_connection = NULL; + manager->cups_connection_timeout_id = 0; + manager->last_notify_sequence_number = -1; + manager->held_jobs = NULL; + + manager->start_idle_id = g_idle_add (gsd_print_notifications_manager_start_idle, manager); + g_source_set_name_by_id (manager->start_idle_id, "[gnome-settings-daemon] gsd_print_notifications_manager_start_idle"); + + gnome_settings_profile_end (NULL); + + return TRUE; +} + +void +gsd_print_notifications_manager_stop (GsdPrintNotificationsManager *manager) +{ + TimeoutData *data; + ReasonData *reason_data; + HeldJob *job; + GList *tmp; + + g_debug ("Stopping print-notifications manager"); + + cupsFreeDests (manager->num_dests, manager->dests); + manager->num_dests = 0; + manager->dests = NULL; + + if (manager->cups_dbus_subscription_id > 0 && + manager->cups_bus_connection != NULL) { + g_dbus_connection_signal_unsubscribe (manager->cups_bus_connection, + manager->cups_dbus_subscription_id); + manager->cups_dbus_subscription_id = 0; + } + + renew_subscription_timeout_enable (manager, FALSE, FALSE); + + if (manager->check_source_id > 0) { + g_source_remove (manager->check_source_id); + manager->check_source_id = 0; + } + + if (manager->subscription_id >= 0) + cancel_subscription (manager->subscription_id); + + g_clear_pointer (&manager->printing_printers, g_hash_table_destroy); + + g_clear_object (&manager->cups_bus_connection); + + for (tmp = manager->timeouts; tmp; tmp = g_list_next (tmp)) { + data = (TimeoutData *) tmp->data; + if (data) + g_source_remove (data->timeout_id); + } + g_list_free_full (manager->timeouts, free_timeout_data); + + for (tmp = manager->active_notifications; tmp; tmp = g_list_next (tmp)) { + reason_data = (ReasonData *) tmp->data; + if (reason_data) { + if (reason_data->notification_close_id > 0 && + g_signal_handler_is_connected (reason_data->notification, + reason_data->notification_close_id)) { + g_signal_handler_disconnect (reason_data->notification, + reason_data->notification_close_id); + reason_data->notification_close_id = 0; + } + + notify_notification_close (reason_data->notification, NULL); + } + } + g_list_free_full (manager->active_notifications, free_reason_data); + + for (tmp = manager->held_jobs; tmp; tmp = g_list_next (tmp)) { + job = (HeldJob *) tmp->data; + g_source_remove (job->timeout_id); + } + g_list_free_full (manager->held_jobs, free_held_job); + + scp_handler (manager, FALSE); +} + +static void +gsd_print_notifications_manager_class_init (GsdPrintNotificationsManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_print_notifications_manager_finalize; + + notify_init ("gnome-settings-daemon"); +} + +static void +gsd_print_notifications_manager_init (GsdPrintNotificationsManager *manager) +{ +} + +static void +gsd_print_notifications_manager_finalize (GObject *object) +{ + GsdPrintNotificationsManager *manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_PRINT_NOTIFICATIONS_MANAGER (object)); + + manager = GSD_PRINT_NOTIFICATIONS_MANAGER (object); + + g_return_if_fail (manager != NULL); + + gsd_print_notifications_manager_stop (manager); + + if (manager->start_idle_id != 0) + g_source_remove (manager->start_idle_id); + + G_OBJECT_CLASS (gsd_print_notifications_manager_parent_class)->finalize (object); +} + +GsdPrintNotificationsManager * +gsd_print_notifications_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_PRINT_NOTIFICATIONS_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_PRINT_NOTIFICATIONS_MANAGER (manager_object); +} diff --git a/plugins/print-notifications/gsd-print-notifications-manager.h b/plugins/print-notifications/gsd-print-notifications-manager.h new file mode 100644 index 0000000..ec1dc72 --- /dev/null +++ b/plugins/print-notifications/gsd-print-notifications-manager.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_PRINT_NOTIFICATIONS_MANAGER_H +#define __GSD_PRINT_NOTIFICATIONS_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_PRINT_NOTIFICATIONS_MANAGER (gsd_print_notifications_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdPrintNotificationsManager, gsd_print_notifications_manager, GSD, PRINT_NOTIFICATIONS_MANAGER, GObject) + +GsdPrintNotificationsManager *gsd_print_notifications_manager_new (void); +gboolean gsd_print_notifications_manager_start (GsdPrintNotificationsManager *manager, + GError **error); +void gsd_print_notifications_manager_stop (GsdPrintNotificationsManager *manager); + +G_END_DECLS + +#endif /* __GSD_PRINT_NOTIFICATIONS_MANAGER_H */ diff --git a/plugins/print-notifications/gsd-printer.c b/plugins/print-notifications/gsd-printer.c new file mode 100644 index 0000000..573129b --- /dev/null +++ b/plugins/print-notifications/gsd-printer.c @@ -0,0 +1,1403 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2011 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" +#include <gio/gio.h> +#include <stdlib.h> +#include <libnotify/notify.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <gtk/gtk.h> +#include <cups/cups.h> +#include <cups/ppd.h> + +static GDBusNodeInfo *npn_introspection_data = NULL; +static GDBusNodeInfo *pdi_introspection_data = NULL; + +#define SCP_DBUS_NPN_NAME "com.redhat.NewPrinterNotification" +#define SCP_DBUS_NPN_PATH "/com/redhat/NewPrinterNotification" +#define SCP_DBUS_NPN_INTERFACE "com.redhat.NewPrinterNotification" + +#define SCP_DBUS_PDI_NAME "com.redhat.PrinterDriversInstaller" +#define SCP_DBUS_PDI_PATH "/com/redhat/PrinterDriversInstaller" +#define SCP_DBUS_PDI_INTERFACE "com.redhat.PrinterDriversInstaller" + +#define PACKAGE_KIT_BUS "org.freedesktop.PackageKit" +#define PACKAGE_KIT_PATH "/org/freedesktop/PackageKit" +#define PACKAGE_KIT_MODIFY_IFACE "org.freedesktop.PackageKit.Modify" +#define PACKAGE_KIT_QUERY_IFACE "org.freedesktop.PackageKit.Query" + +#define SCP_BUS "org.fedoraproject.Config.Printing" +#define SCP_PATH "/org/fedoraproject/Config/Printing" +#define SCP_IFACE "org.fedoraproject.Config.Printing" + +#define MECHANISM_BUS "org.opensuse.CupsPkHelper.Mechanism" + +#define ALLOWED_CHARACTERS "abcdefghijklmnopqrtsuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" + +#define DBUS_TIMEOUT 60000 +#define DBUS_INSTALL_TIMEOUT 3600000 + +#define GNOME_SESSION_DBUS_NAME "org.gnome.SessionManager" +#define GNOME_SESSION_DBUS_PATH "/org/gnome/SessionManager" +#define GNOME_SESSION_DBUS_IFACE "org.gnome.SessionManager" +#define GNOME_SESSION_CLIENT_PRIVATE_DBUS_IFACE "org.gnome.SessionManager.ClientPrivate" + +#define GNOME_SESSION_PRESENCE_DBUS_PATH "/org/gnome/SessionManager/Presence" +#define GNOME_SESSION_PRESENCE_DBUS_IFACE "org.gnome.SessionManager.Presence" + +#if (CUPS_VERSION_MAJOR > 1) || (CUPS_VERSION_MINOR > 5) +#define HAVE_CUPS_1_6 1 +#endif + +#ifndef HAVE_CUPS_1_6 +#define ippGetState(ipp) ipp->state +#endif + +enum { + PRESENCE_STATUS_AVAILABLE = 0, + PRESENCE_STATUS_INVISIBLE, + PRESENCE_STATUS_BUSY, + PRESENCE_STATUS_IDLE, + PRESENCE_STATUS_UNKNOWN +}; + +static const gchar npn_introspection_xml[] = + "<node name='/com/redhat/NewPrinterNotification'>" + " <interface name='com.redhat.NewPrinterNotification'>" + " <method name='GetReady'>" + " </method>" + " <method name='NewPrinter'>" + " <arg type='i' name='status' direction='in'/>" + " <arg type='s' name='name' direction='in'/>" + " <arg type='s' name='mfg' direction='in'/>" + " <arg type='s' name='mdl' direction='in'/>" + " <arg type='s' name='des' direction='in'/>" + " <arg type='s' name='cmd' direction='in'/>" + " </method>" + " </interface>" + "</node>"; + +static const gchar pdi_introspection_xml[] = + "<node name='/com/redhat/PrinterDriversInstaller'>" + " <interface name='com.redhat.PrinterDriversInstaller'>" + " <method name='InstallDrivers'>" + " <arg type='s' name='mfg' direction='in'/>" + " <arg type='s' name='mdl' direction='in'/>" + " <arg type='s' name='cmd' direction='in'/>" + " </method>" + " </interface>" + "</node>"; + +static GMainLoop *main_loop; +static guint npn_registration_id; +static guint pdi_registration_id; +static guint npn_owner_id; +static guint pdi_owner_id; + +static GHashTable * +get_missing_executables (const gchar *ppd_file_name) +{ + GHashTable *executables = NULL; + GDBusProxy *proxy; + GVariant *output; + GVariant *array; + GError *error = NULL; + gint i; + + if (!ppd_file_name) + return NULL; + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + SCP_BUS, + SCP_PATH, + SCP_IFACE, + NULL, + &error); + + if (!proxy) { + g_warning ("%s", error->message); + g_error_free (error); + return NULL; + } + + output = g_dbus_proxy_call_sync (proxy, + "MissingExecutables", + g_variant_new ("(s)", + ppd_file_name), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT, + NULL, + &error); + + if (output && g_variant_n_children (output) == 1) { + array = g_variant_get_child_value (output, 0); + if (array) { + executables = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + for (i = 0; i < g_variant_n_children (array); i++) { + g_hash_table_insert (executables, + g_strdup (g_variant_get_string ( + g_variant_get_child_value (array, i), + NULL)), + NULL); + } + } + } + + if (output) { + g_variant_unref (output); + } else { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_object_unref (proxy); + + return executables; +} + +static GHashTable * +find_packages_for_executables (GHashTable *executables) +{ + GHashTableIter exec_iter; + GHashTable *packages = NULL; + GDBusProxy *proxy; + GVariant *output; + gpointer key, value; + GError *error = NULL; + + if (!executables || g_hash_table_size (executables) <= 0) + return NULL; + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + PACKAGE_KIT_BUS, + PACKAGE_KIT_PATH, + PACKAGE_KIT_QUERY_IFACE, + NULL, + &error); + + if (!proxy) { + g_warning ("%s", error->message); + g_error_free (error); + return NULL; + } + + packages = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + + g_hash_table_iter_init (&exec_iter, executables); + while (g_hash_table_iter_next (&exec_iter, &key, &value)) { + output = g_dbus_proxy_call_sync (proxy, + "SearchFile", + g_variant_new ("(ss)", + (gchar *) key, + ""), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT, + NULL, + &error); + + if (output) { + gboolean installed; + gchar *package; + + g_variant_get (output, + "(bs)", + &installed, + &package); + if (!installed) + g_hash_table_insert (packages, g_strdup (package), NULL); + + g_variant_unref (output); + } else { + g_warning ("%s", error->message); + g_error_free (error); + } + } + + g_object_unref (proxy); + + return packages; +} + +static void +install_packages (GHashTable *packages) +{ + GVariantBuilder array_builder; + GHashTableIter pkg_iter; + GDBusProxy *proxy; + GVariant *output; + gpointer key, value; + GError *error = NULL; + + if (!packages || g_hash_table_size (packages) <= 0) + return; + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + PACKAGE_KIT_BUS, + PACKAGE_KIT_PATH, + PACKAGE_KIT_MODIFY_IFACE, + NULL, + &error); + + if (!proxy) { + g_warning ("%s", error->message); + g_error_free (error); + return; + } + + g_variant_builder_init (&array_builder, G_VARIANT_TYPE ("as")); + + g_hash_table_iter_init (&pkg_iter, packages); + while (g_hash_table_iter_next (&pkg_iter, &key, &value)) { + g_variant_builder_add (&array_builder, + "s", + (gchar *) key); + } + + output = g_dbus_proxy_call_sync (proxy, + "InstallPackageNames", + g_variant_new ("(uass)", + 0, + &array_builder, + "hide-finished"), + G_DBUS_CALL_FLAGS_NONE, + DBUS_INSTALL_TIMEOUT, + NULL, + &error); + + if (output) { + g_variant_unref (output); + } else { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_object_unref (proxy); +} + +static gchar * +get_best_ppd (gchar *device_id, + gchar *device_make_and_model, + gchar *device_uri) +{ + GDBusProxy *proxy; + GVariant *output; + GVariant *array; + GVariant *tuple; + GError *error = NULL; + gchar *ppd_name = NULL; + gint i, j; + static const char * const match_levels[] = { + "exact-cmd", + "exact", + "close", + "generic", + "none"}; + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + SCP_BUS, + SCP_PATH, + SCP_IFACE, + NULL, + &error); + + if (!proxy) { + g_warning ("%s", error->message); + g_error_free (error); + return NULL; + } + + output = g_dbus_proxy_call_sync (proxy, + "GetBestDrivers", + g_variant_new ("(sss)", + device_id ? device_id : "", + device_make_and_model ? device_make_and_model : "", + device_uri ? device_uri : ""), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT, + NULL, + &error); + + if (output && g_variant_n_children (output) >= 1) { + array = g_variant_get_child_value (output, 0); + if (array) + for (j = 0; j < G_N_ELEMENTS (match_levels) && ppd_name == NULL; j++) + for (i = 0; i < g_variant_n_children (array) && ppd_name == NULL; i++) { + tuple = g_variant_get_child_value (array, i); + if (tuple && g_variant_n_children (tuple) == 2) { + if (g_strcmp0 (g_variant_get_string ( + g_variant_get_child_value (tuple, 1), + NULL), match_levels[j]) == 0) + ppd_name = g_strdup (g_variant_get_string ( + g_variant_get_child_value (tuple, 0), + NULL)); + } + } + } + + if (output) { + g_variant_unref (output); + } else { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_object_unref (proxy); + + return ppd_name; +} + +static gchar * +get_tag_value (const gchar *tag_string, + const gchar *tag_name) +{ + gchar **tag_string_splitted; + gchar *tag_value = NULL; + gint tag_name_length; + gint i; + + if (!tag_string || + !tag_name) + return NULL; + + tag_name_length = strlen (tag_name); + tag_string_splitted = g_strsplit (tag_string, ";", 0); + if (tag_string_splitted) { + for (i = 0; i < g_strv_length (tag_string_splitted); i++) + if (g_ascii_strncasecmp (tag_string_splitted[i], tag_name, tag_name_length) == 0) + if (strlen (tag_string_splitted[i]) > tag_name_length + 1) + tag_value = g_strdup (tag_string_splitted[i] + tag_name_length + 1); + + g_strfreev (tag_string_splitted); + } + + return tag_value; +} + +static gchar * +create_name (gchar *device_id) +{ + cups_dest_t *dests; + gboolean already_present = FALSE; + gchar *name = NULL; + gchar *new_name = NULL; + gint num_dests; + gint name_index = 2; + gint j; + + g_return_val_if_fail (device_id != NULL, NULL); + + name = get_tag_value (device_id, "mdl"); + if (!name) + name = get_tag_value (device_id, "model"); + + if (name) + name = g_strcanon (name, ALLOWED_CHARACTERS, '-'); + + num_dests = cupsGetDests (&dests); + do { + if (already_present) { + new_name = g_strdup_printf ("%s-%d", name, name_index); + name_index++; + } else { + new_name = g_strdup (name); + } + + already_present = FALSE; + for (j = 0; j < num_dests; j++) + if (g_strcmp0 (dests[j].name, new_name) == 0) + already_present = TRUE; + + if (already_present) { + g_free (new_name); + } else { + g_free (name); + name = new_name; + } + } while (already_present); + cupsFreeDests (num_dests, dests); + + return name; +} + +static gboolean +add_printer (gchar *printer_name, + gchar *device_uri, + gchar *ppd_name, + gchar *info, + gchar *location) +{ + cups_dest_t *dests; + GDBusProxy *proxy; + gboolean success = FALSE; + GVariant *output; + GError *error = NULL; + gint num_dests; + gint i; + + if (!printer_name || !device_uri || !ppd_name) + return FALSE; + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + MECHANISM_BUS, + "/", + MECHANISM_BUS, + NULL, + &error); + + if (!proxy) { + g_warning ("%s", error->message); + g_error_free (error); + return FALSE; + } + + output = g_dbus_proxy_call_sync (proxy, + "PrinterAdd", + g_variant_new ("(sssss)", + printer_name, + device_uri, + ppd_name, + info ? info : "", + location ? location : ""), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT, + NULL, + &error); + + if (output) { + g_variant_unref (output); + } else { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_object_unref (proxy); + + num_dests = cupsGetDests (&dests); + for (i = 0; i < num_dests; i++) + if (g_strcmp0 (dests[i].name, printer_name) == 0) + success = TRUE; + cupsFreeDests (num_dests, dests); + + return success; +} + +static gboolean +printer_set_enabled (const gchar *printer_name, + gboolean enabled) +{ + GDBusProxy *proxy; + gboolean result = TRUE; + GVariant *output; + GError *error = NULL; + + if (!printer_name) + return FALSE; + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + MECHANISM_BUS, + "/", + MECHANISM_BUS, + NULL, + &error); + + if (!proxy) { + g_warning ("%s", error->message); + g_error_free (error); + return FALSE; + } + + output = g_dbus_proxy_call_sync (proxy, + "PrinterSetEnabled", + g_variant_new ("(sb)", + printer_name, + enabled), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT, + NULL, + &error); + + if (output) { + g_variant_unref (output); + } else { + g_warning ("%s", error->message); + g_error_free (error); + result = FALSE; + } + + g_object_unref (proxy); + + return result; +} + +static gboolean +printer_set_accepting_jobs (const gchar *printer_name, + gboolean accepting_jobs, + const gchar *reason) +{ + GDBusProxy *proxy; + gboolean result = TRUE; + GVariant *output; + GError *error = NULL; + + if (!printer_name) + return FALSE; + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + MECHANISM_BUS, + "/", + MECHANISM_BUS, + NULL, + &error); + + if (!proxy) { + g_warning ("%s", error->message); + g_error_free (error); + return FALSE; + } + + output = g_dbus_proxy_call_sync (proxy, + "PrinterSetAcceptJobs", + g_variant_new ("(sbs)", + printer_name, + accepting_jobs, + reason ? reason : ""), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT, + NULL, + &error); + + if (output) { + g_variant_unref (output); + } else { + g_warning ("%s", error->message); + g_error_free (error); + result = FALSE; + } + + g_object_unref (proxy); + + return result; +} + +static ipp_t * +execute_maintenance_command (const char *printer_name, + const char *command, + const char *title) +{ + http_t *http; + GError *error = NULL; + ipp_t *request = NULL; + ipp_t *response = NULL; + gchar *file_name = NULL; + char *uri; + int fd = -1; + + http = httpConnectEncrypt (cupsServer (), + ippPort (), + cupsEncryption ()); + + if (!http) + return NULL; + + request = ippNewRequest (IPP_PRINT_JOB); + + uri = g_strdup_printf ("ipp://localhost/printers/%s", + printer_name); + + ippAddString (request, + IPP_TAG_OPERATION, + IPP_TAG_URI, + "printer-uri", + NULL, + uri); + + g_free (uri); + + ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_NAME, "job-name", + NULL, title); + + ippAddString (request, IPP_TAG_JOB, IPP_TAG_MIMETYPE, "document-format", + NULL, "application/vnd.cups-command"); + + fd = g_file_open_tmp ("ccXXXXXX", &file_name, &error); + + if (fd != -1) { + FILE *file; + + file = fdopen (fd, "w"); + fprintf (file, "#CUPS-COMMAND\n"); + fprintf (file, "%s\n", command); + fclose (file); + + response = cupsDoFileRequest (http, request, "/", file_name); + g_unlink (file_name); + } else { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_free (file_name); + httpClose (http); + + return response; +} + +static char * +get_dest_attr (const char *dest_name, + const char *attr) +{ + cups_dest_t *dests; + int num_dests; + cups_dest_t *dest; + const char *value; + char *ret; + + if (dest_name == NULL) + return NULL; + + ret = NULL; + + num_dests = cupsGetDests (&dests); + if (num_dests < 1) { + g_debug ("Unable to get printer destinations"); + return NULL; + } + + dest = cupsGetDest (dest_name, NULL, num_dests, dests); + if (dest == NULL) { + g_debug ("Unable to find a printer named '%s'", dest_name); + goto out; + } + + value = cupsGetOption (attr, dest->num_options, dest->options); + if (value == NULL) { + g_debug ("Unable to get %s for '%s'", attr, dest_name); + goto out; + } + ret = g_strdup (value); +out: + cupsFreeDests (num_dests, dests); + + return ret; +} + +static void +printer_autoconfigure (gchar *printer_name) +{ + gchar *commands; + gchar *commands_lowercase; + ipp_t *response = NULL; + + if (!printer_name) + return; + + commands = get_dest_attr (printer_name, "printer-commands"); + commands_lowercase = g_ascii_strdown (commands, -1); + + if (g_strrstr (commands_lowercase, "autoconfigure")) { + response = execute_maintenance_command (printer_name, + "AutoConfigure", + ("Automatic configuration")); + if (response) { + if (ippGetState (response) == IPP_ERROR) + g_warning ("An error has occured during automatic configuration of new printer."); + ippDelete (response); + } + } + g_free (commands); + g_free (commands_lowercase); +} + +/* Returns default page size for current locale */ +static const gchar * +get_page_size_from_locale (void) +{ + if (g_str_equal (gtk_paper_size_get_default (), GTK_PAPER_NAME_LETTER)) + return "Letter"; + else + return "A4"; +} + +static void +set_default_paper_size (const gchar *printer_name, + const gchar *ppd_file_name) +{ + GDBusProxy *proxy; + GVariant *output; + GError *error = NULL; + GVariantBuilder *builder; + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + MECHANISM_BUS, + "/", + MECHANISM_BUS, + NULL, + &error); + + if (!proxy) { + g_warning ("%s", error->message); + g_error_free (error); + return; + } + + /* Set default media size according to the locale + * FIXME: Handle more than A4 and Letter: + * https://bugzilla.gnome.org/show_bug.cgi?id=660769 */ + builder = g_variant_builder_new (G_VARIANT_TYPE ("as")); + g_variant_builder_add (builder, "s", get_page_size_from_locale ()); + + output = g_dbus_proxy_call_sync (proxy, + "PrinterAddOption", + g_variant_new ("(ssas)", + printer_name ? printer_name : "", + "PageSize", + builder), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT, + NULL, + &error); + + if (output) { + g_variant_unref (output); + } else { + if (!(error->domain == G_DBUS_ERROR && + (error->code == G_DBUS_ERROR_SERVICE_UNKNOWN || + error->code == G_DBUS_ERROR_UNKNOWN_METHOD))) + g_warning ("%s", error->message); + g_error_free (error); + } + + g_object_unref (proxy); +} + +/* + * Setup new printer and returns TRUE if successful. + */ +static gboolean +setup_printer (gchar *device_id, + gchar *device_make_and_model, + gchar *device_uri) +{ + gboolean success = FALSE; + gchar *ppd_name; + gchar *printer_name; + + ppd_name = get_best_ppd (device_id, device_make_and_model, device_uri); + printer_name = create_name (device_id); + + if (!ppd_name || !printer_name || !device_uri) { + g_free (ppd_name); + g_free (printer_name); + return FALSE; + } + + success = add_printer (printer_name, device_uri, + ppd_name, NULL, NULL); + + /* Set some options of the new printer */ + if (success) { + const char *ppd_file_name; + + printer_set_accepting_jobs (printer_name, TRUE, NULL); + printer_set_enabled (printer_name, TRUE); + printer_autoconfigure (printer_name); + + ppd_file_name = cupsGetPPD (printer_name); + + if (ppd_file_name) { + GHashTable *executables; + GHashTable *packages; + + set_default_paper_size (printer_name, ppd_file_name); + + executables = get_missing_executables (ppd_file_name); + packages = find_packages_for_executables (executables); + install_packages (packages); + + if (executables) + g_hash_table_destroy (executables); + if (packages) + g_hash_table_destroy (packages); + g_unlink (ppd_file_name); + } + } + + g_free (printer_name); + g_free (ppd_name); + + return success; +} + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + gchar *primary_text = NULL; + gchar *secondary_text = NULL; + gchar *name = NULL; + gchar *mfg = NULL; + gchar *mdl = NULL; + gchar *des = NULL; + gchar *cmd = NULL; + gchar *device = NULL; + gchar *device_id; + gchar *make_and_model; + gint status = 0; + + if (g_strcmp0 (method_name, "GetReady") == 0) { + /* Translators: We are configuring new printer */ + primary_text = g_strdup (_("Configuring new printer")); + /* Translators: Just wait */ + secondary_text = g_strdup (_("Please wait…")); + + g_dbus_method_invocation_return_value (invocation, + NULL); + } + else if (g_strcmp0 (method_name, "NewPrinter") == 0) { + if (g_variant_n_children (parameters) == 6) { + g_variant_get (parameters, "(i&s&s&s&s&s)", + &status, + &name, + &mfg, + &mdl, + &des, + &cmd); + } + + if (g_strrstr (name, "/")) { + /* name is a URI, no queue was generated, because no suitable + * driver was found + */ + + device_id = g_strdup_printf ("MFG:%s;MDL:%s;DES:%s;CMD:%s;", mfg, mdl, des, cmd); + make_and_model = g_strdup_printf ("%s %s", mfg, mdl); + + if (!setup_printer (device_id, make_and_model, name)) { + + /* Translators: We have no driver installed for this printer */ + primary_text = g_strdup (_("Missing printer driver")); + + if ((mfg && mdl) || des) { + if (mfg && mdl) + device = g_strdup_printf ("%s %s", mfg, mdl); + else + device = g_strdup (des); + + /* Translators: We have no driver installed for the device */ + secondary_text = g_strdup_printf (_("No printer driver for %s."), device); + g_free (device); + } + else + /* Translators: We have no driver installed for this printer */ + secondary_text = g_strdup (_("No driver for this printer.")); + } + + g_free (make_and_model); + g_free (device_id); + } + else { + /* name is the name of the queue which hal_lpadmin has set up + * automatically. + */ + + const char *ppd_file_name; + + ppd_file_name = cupsGetPPD (name); + if (ppd_file_name) { + GHashTable *executables; + GHashTable *packages; + + executables = get_missing_executables (ppd_file_name); + packages = find_packages_for_executables (executables); + install_packages (packages); + + if (executables) + g_hash_table_destroy (executables); + if (packages) + g_hash_table_destroy (packages); + g_unlink (ppd_file_name); + } + } + + g_dbus_method_invocation_return_value (invocation, + NULL); + } + else if (g_strcmp0 (method_name, "InstallDrivers") == 0) { + GDBusProxy *proxy; + GError *error = NULL; + + if (g_variant_n_children (parameters) == 3) { + g_variant_get (parameters, "(&s&s&s)", + &mfg, + &mdl, + &cmd); + } + + if (mfg && mdl) + device = g_strdup_printf ("MFG:%s;MDL:%s;", mfg, mdl); + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + PACKAGE_KIT_BUS, + PACKAGE_KIT_PATH, + PACKAGE_KIT_MODIFY_IFACE, + NULL, + &error); + + if (!proxy) { + g_warning ("%s", error->message); + g_error_free (error); + } + + if (proxy && device) { + GVariantBuilder *builder; + GVariant *output; + + builder = g_variant_builder_new (G_VARIANT_TYPE ("as")); + g_variant_builder_add (builder, "s", device); + + output = g_dbus_proxy_call_sync (proxy, + "InstallPrinterDrivers", + g_variant_new ("(uass)", + 0, + builder, + "hide-finished"), + G_DBUS_CALL_FLAGS_NONE, + DBUS_INSTALL_TIMEOUT, + NULL, + &error); + + if (output) { + g_variant_unref (output); + } else { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_object_unref (proxy); + } + + g_dbus_method_invocation_return_value (invocation, + NULL); + } + + if (primary_text) { + NotifyNotification *notification; + notification = notify_notification_new (primary_text, + secondary_text, + "printer-symbolic"); + notify_notification_set_app_name (notification, _("Printers")); + notify_notification_set_hint_string (notification, "desktop-entry", "gnome-printers-panel"); + notify_notification_set_hint (notification, "transient", g_variant_new_boolean (TRUE)); + + notify_notification_show (notification, NULL); + g_object_unref (notification); + g_free (primary_text); + g_free (secondary_text); + } +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + NULL, + NULL +}; + +static void +unregister_objects () +{ + GDBusConnection *system_connection; + GError *error = NULL; + + system_connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error); + + if (npn_registration_id > 0) { + g_dbus_connection_unregister_object (system_connection, npn_registration_id); + npn_registration_id = 0; + } + + if (pdi_registration_id > 0) { + g_dbus_connection_unregister_object (system_connection, pdi_registration_id); + pdi_registration_id = 0; + } +} + +static void +unown_names () +{ + if (npn_owner_id > 0) { + g_bus_unown_name (npn_owner_id); + npn_owner_id = 0; + } + + if (pdi_owner_id > 0) { + g_bus_unown_name (pdi_owner_id); + pdi_owner_id = 0; + } +} + +static void +on_npn_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GError *error = NULL; + + npn_registration_id = g_dbus_connection_register_object (connection, + SCP_DBUS_NPN_PATH, + npn_introspection_data->interfaces[0], + &interface_vtable, + NULL, + NULL, + &error); + + if (npn_registration_id == 0) { + g_warning ("Failed to register object: %s\n", error->message); + g_error_free (error); + } +} + +static void +on_pdi_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GError *error = NULL; + + pdi_registration_id = g_dbus_connection_register_object (connection, + SCP_DBUS_PDI_PATH, + pdi_introspection_data->interfaces[0], + &interface_vtable, + NULL, + NULL, + &error); + + if (pdi_registration_id == 0) { + g_warning ("Failed to register object: %s\n", error->message); + g_error_free (error); + } +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + unregister_objects (); +} + +static void +session_signal_handler (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + guint new_status; + + g_variant_get (parameters, "(u)", &new_status); + + if (new_status == PRESENCE_STATUS_IDLE || + new_status == PRESENCE_STATUS_AVAILABLE) { + unregister_objects (); + unown_names (); + + if (new_status == PRESENCE_STATUS_AVAILABLE) { + npn_owner_id = g_bus_own_name (G_BUS_TYPE_SYSTEM, + SCP_DBUS_NPN_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_npn_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + pdi_owner_id = g_bus_own_name (G_BUS_TYPE_SYSTEM, + SCP_DBUS_PDI_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_pdi_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + } + } +} + +static void +client_signal_handler (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GDBusProxy *proxy; + GError *error = NULL; + GVariant *output; + + if (g_strcmp0 (signal_name, "QueryEndSession") == 0 || + g_strcmp0 (signal_name, "EndSession") == 0) { + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + sender_name, + object_path, + interface_name, + NULL, + &error); + + if (proxy) { + output = g_dbus_proxy_call_sync (proxy, + "EndSessionResponse", + g_variant_new ("(bs)", TRUE, ""), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &error); + + if (output) { + g_variant_unref (output); + } + else { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_object_unref (proxy); + } + else { + g_warning ("%s", error->message); + g_error_free (error); + } + + if (g_strcmp0 (signal_name, "EndSession") == 0) { + g_main_loop_quit (main_loop); + g_debug ("Exiting gsd-printer"); + } + } +} + +static gchar * +register_gnome_session_client (const gchar *app_id, + const gchar *client_startup_id) +{ + GDBusProxy *proxy; + GVariant *output = NULL; + GError *error = NULL; + const gchar *client_id = NULL; + gchar *result = NULL; + + proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + GNOME_SESSION_DBUS_NAME, + GNOME_SESSION_DBUS_PATH, + GNOME_SESSION_DBUS_IFACE, + NULL, + &error); + + if (proxy) { + output = g_dbus_proxy_call_sync (proxy, + "RegisterClient", + g_variant_new ("(ss)", app_id, client_startup_id), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &error); + + if (output) { + g_variant_get (output, "(o)", &client_id); + if (client_id) + result = g_strdup (client_id); + g_variant_unref (output); + } + else { + g_warning ("%s", error->message); + g_error_free (error); + } + + g_object_unref (proxy); + } + else { + g_warning ("%s", error->message); + g_error_free (error); + } + + return result; +} + +int +main (int argc, char *argv[]) +{ + GDBusConnection *connection; + gboolean client_signal_subscription_set = FALSE; + GError *error = NULL; + guint client_signal_subscription_id; + guint session_signal_subscription_id; + gchar *object_path; + + bindtextdomain (GETTEXT_PACKAGE, GNOME_SETTINGS_LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + setlocale (LC_ALL, ""); + + npn_registration_id = 0; + pdi_registration_id = 0; + npn_owner_id = 0; + pdi_owner_id = 0; + + notify_init ("gnome-settings-daemon-printer"); + + npn_introspection_data = + g_dbus_node_info_new_for_xml (npn_introspection_xml, &error); + + if (npn_introspection_data == NULL) { + g_warning ("Error parsing introspection XML: %s\n", error->message); + g_error_free (error); + goto error; + } + + pdi_introspection_data = + g_dbus_node_info_new_for_xml (pdi_introspection_xml, &error); + + if (pdi_introspection_data == NULL) { + g_warning ("Error parsing introspection XML: %s\n", error->message); + g_error_free (error); + goto error; + } + + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + + session_signal_subscription_id = + g_dbus_connection_signal_subscribe (connection, + NULL, + GNOME_SESSION_PRESENCE_DBUS_IFACE, + "StatusChanged", + GNOME_SESSION_PRESENCE_DBUS_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + session_signal_handler, + NULL, + NULL); + + object_path = register_gnome_session_client ("gsd-printer", ""); + if (object_path) { + client_signal_subscription_id = + g_dbus_connection_signal_subscribe (connection, + NULL, + GNOME_SESSION_CLIENT_PRIVATE_DBUS_IFACE, + NULL, + object_path, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + client_signal_handler, + NULL, + NULL); + client_signal_subscription_set = TRUE; + } + + if (npn_owner_id == 0) + npn_owner_id = g_bus_own_name (G_BUS_TYPE_SYSTEM, + SCP_DBUS_NPN_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_npn_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + if (pdi_owner_id == 0) + pdi_owner_id = g_bus_own_name (G_BUS_TYPE_SYSTEM, + SCP_DBUS_PDI_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_pdi_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + main_loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (main_loop); + + unregister_objects (); + unown_names (); + + if (client_signal_subscription_set) + g_dbus_connection_signal_unsubscribe (connection, client_signal_subscription_id); + g_dbus_connection_signal_unsubscribe (connection, session_signal_subscription_id); + + g_free (object_path); + + g_dbus_node_info_unref (npn_introspection_data); + g_dbus_node_info_unref (pdi_introspection_data); + + return 0; + +error: + + if (npn_introspection_data) + g_dbus_node_info_unref (npn_introspection_data); + + if (pdi_introspection_data) + g_dbus_node_info_unref (pdi_introspection_data); + + return 1; +} diff --git a/plugins/print-notifications/main.c b/plugins/print-notifications/main.c new file mode 100644 index 0000000..a0dd406 --- /dev/null +++ b/plugins/print-notifications/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_print_notifications_manager_new +#define START gsd_print_notifications_manager_start +#define STOP gsd_print_notifications_manager_stop +#define MANAGER GsdPrintNotificationsManager +#include "gsd-print-notifications-manager.h" + +#include "daemon-skeleton.h" diff --git a/plugins/print-notifications/meson.build b/plugins/print-notifications/meson.build new file mode 100644 index 0000000..1e1c614 --- /dev/null +++ b/plugins/print-notifications/meson.build @@ -0,0 +1,37 @@ +sources = files( + 'gsd-print-notifications-manager.c', + 'main.c' +) + +deps = plugins_deps + [ + cups_dep, + gtk_dep, + libnotify_dep +] + +cflags += ['-DLIBEXECDIR="@0@"'.format(gsd_libexecdir)] +cflags += ['-DBINDIR="@0@"'.format(gsd_bindir)] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) + +program = 'gsd-printer' + +executable( + program, + program + '.c', + include_directories: top_inc, + dependencies: deps, + c_args: '-DGNOME_SETTINGS_LOCALEDIR="@0@"'.format(gsd_localedir), + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) diff --git a/plugins/rfkill/61-gnome-settings-daemon-rfkill.rules b/plugins/rfkill/61-gnome-settings-daemon-rfkill.rules new file mode 100644 index 0000000..87eabff --- /dev/null +++ b/plugins/rfkill/61-gnome-settings-daemon-rfkill.rules @@ -0,0 +1,8 @@ +# Get access to /dev/rfkill for users +# See https://bugzilla.redhat.com/show_bug.cgi?id=514798 +# +# Simplified by Kay Sievers +# https://bugzilla.redhat.com/show_bug.cgi?id=733326 +# See also https://bugzilla.gnome.org/show_bug.cgi?id=711373 + +KERNEL=="rfkill", SUBSYSTEM=="misc", TAG+="uaccess" diff --git a/plugins/rfkill/gsd-rfkill-manager.c b/plugins/rfkill/gsd-rfkill-manager.c new file mode 100644 index 0000000..5c8b690 --- /dev/null +++ b/plugins/rfkill/gsd-rfkill-manager.c @@ -0,0 +1,902 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2010,2011 Red Hat, Inc. + * + * Author: Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +/* Test with: + * gdbus call \ + * --session \ + * --dest org.gnome.SettingsDaemon.Rfkill \ + * --object-path /org/gnome/SettingsDaemon/Rfkill \ + * --method org.freedesktop.DBus.Properties.Set \ + * "org.gnome.SettingsDaemon.Rfkill" \ + * "AirplaneMode" \ + * "<true|false>" + * and + * gdbus call \ + * --session \ + * --dest org.gnome.SettingsDaemon.Rfkill \ + * --object-path /org/gnome/SettingsDaemon/Rfkill \ + * --method org.freedesktop.DBus.Properties.Set \ + * "org.gnome.SettingsDaemon.Rfkill" \ + * "BluetoothAirplaneMode" \ + * "<true|false>" + * + * and + * gdbus call \ + * --session \ + * --dest org.gnome.SettingsDaemon.Rfkill \ + * --object-path /org/gnome/SettingsDaemon/Rfkill \ + * --method org.freedesktop.DBus.Properties.Set \ + * "org.gnome.SettingsDaemon.Rfkill" \ + * "WwanAirplaneMode" \ + * "<true|false>" + */ + +#include "config.h" + +#include <gio/gio.h> +#include <string.h> + +#include "gnome-settings-profile.h" +#include "gsd-rfkill-manager.h" +#include "rfkill-glib.h" +#include "gnome-settings-bus.h" + +struct _GsdRfkillManager +{ + GObject parent; + + GDBusNodeInfo *introspection_data; + guint name_id; + GDBusConnection *connection; + GCancellable *cancellable; + + CcRfkillGlib *rfkill; + GHashTable *killswitches; + GHashTable *bt_killswitches; + GHashTable *wwan_killswitches; + + /* In addition to using the rfkill kernel subsystem + (which is exposed by wlan, wimax, bluetooth, nfc, + some platform drivers and some usb modems), we + need to go through NetworkManager, which in turn + will tell ModemManager to write the right commands + in the USB bus to take external modems down, all + from userspace. + */ + GDBusProxy *nm_client; + gboolean wwan_enabled; + GDBusObjectManager *mm_client; + gboolean wwan_interesting; + + GsdSessionManager *session; + GBinding *rfkill_input_inhibit_binding; + + gchar *chassis_type; +}; + +#define GSD_DBUS_NAME "org.gnome.SettingsDaemon" +#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon" +#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon" + +#define GSD_RFKILL_DBUS_NAME GSD_DBUS_NAME ".Rfkill" +#define GSD_RFKILL_DBUS_PATH GSD_DBUS_PATH "/Rfkill" + +static const gchar introspection_xml[] = +"<node>" +" <interface name='org.gnome.SettingsDaemon.Rfkill'>" +" <annotation name='org.freedesktop.DBus.GLib.CSymbol' value='gsd_rfkill_manager'/>" +" <property name='AirplaneMode' type='b' access='readwrite'/>" +" <property name='HardwareAirplaneMode' type='b' access='read'/>" +" <property name='HasAirplaneMode' type='b' access='read'/>" +" <property name='ShouldShowAirplaneMode' type='b' access='read'/>" +" <property name='BluetoothAirplaneMode' type='b' access='readwrite'/>" +" <property name='BluetoothHardwareAirplaneMode' type='b' access='read'/>" +" <property name='BluetoothHasAirplaneMode' type='b' access='read'/>" +" <property name='WwanAirplaneMode' type='b' access='readwrite'/>" +" <property name='WwanHardwareAirplaneMode' type='b' access='read'/>" +" <property name='WwanHasAirplaneMode' type='b' access='read'/>" +" </interface>" +"</node>"; + +static void gsd_rfkill_manager_class_init (GsdRfkillManagerClass *klass); +static void gsd_rfkill_manager_init (GsdRfkillManager *rfkill_manager); +static void gsd_rfkill_manager_finalize (GObject *object); + +G_DEFINE_TYPE (GsdRfkillManager, gsd_rfkill_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +static void +gsd_rfkill_manager_class_init (GsdRfkillManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_rfkill_manager_finalize; +} + +static void +gsd_rfkill_manager_init (GsdRfkillManager *manager) +{ +} + +static gboolean +engine_get_airplane_mode_helper (GHashTable *killswitches) +{ + GHashTableIter iter; + gpointer key, value; + + if (g_hash_table_size (killswitches) == 0) + return FALSE; + + g_hash_table_iter_init (&iter, killswitches); + while (g_hash_table_iter_next (&iter, &key, &value)) { + int state; + + state = GPOINTER_TO_INT (value); + + /* A single rfkill switch that's unblocked? Airplane mode is off */ + if (state == RFKILL_STATE_UNBLOCKED) + return FALSE; + } + + return TRUE; +} + +static gboolean +engine_get_bluetooth_airplane_mode (GsdRfkillManager *manager) +{ + return engine_get_airplane_mode_helper (manager->bt_killswitches); +} + +static gboolean +engine_get_bluetooth_hardware_airplane_mode (GsdRfkillManager *manager) +{ + GHashTableIter iter; + gpointer key, value; + + /* If we have no killswitches, hw airplane mode is off. */ + if (g_hash_table_size (manager->bt_killswitches) == 0) + return FALSE; + + g_hash_table_iter_init (&iter, manager->bt_killswitches); + while (g_hash_table_iter_next (&iter, &key, &value)) { + int state; + + state = GPOINTER_TO_INT (value); + + /* A single rfkill switch that's not hw blocked? Hw airplane mode is off */ + if (state != RFKILL_STATE_HARD_BLOCKED) { + return FALSE; + } + } + + return TRUE; +} + +static gboolean +engine_get_has_bluetooth_airplane_mode (GsdRfkillManager *manager) +{ + return (g_hash_table_size (manager->bt_killswitches) > 0); +} + +static gboolean +engine_get_wwan_airplane_mode (GsdRfkillManager *manager) +{ + gboolean is_airplane; + + is_airplane = engine_get_airplane_mode_helper (manager->wwan_killswitches); + + /* Try our luck with Modem Manager too. Check only if no rfkill + * devices found, or if rfkill reports all devices to be down. + * (Airplane mode will be disabled if at least one device is up, + * so if rfkill says no device is up, check any device is up via + * Network Manager (which in turn, is handled via Modem Manager)) + */ + if (g_hash_table_size (manager->wwan_killswitches) == 0 || is_airplane) + if (manager->wwan_interesting) + is_airplane = !manager->wwan_enabled; + + return is_airplane; +} + +static gboolean +engine_get_wwan_hardware_airplane_mode (GsdRfkillManager *manager) +{ + GHashTableIter iter; + gpointer key, value; + + /* If we have no killswitches, hw airplane mode is off. */ + if (g_hash_table_size (manager->wwan_killswitches) == 0) + return FALSE; + + g_hash_table_iter_init (&iter, manager->wwan_killswitches); + while (g_hash_table_iter_next (&iter, &key, &value)) { + int state; + + state = GPOINTER_TO_INT (value); + + /* A single rfkill switch that's not hw blocked? Hw airplane mode is off */ + if (state != RFKILL_STATE_HARD_BLOCKED) { + return FALSE; + } + } + + return TRUE; +} + +static gboolean +engine_get_has_wwan_airplane_mode (GsdRfkillManager *manager) +{ + return (g_hash_table_size (manager->wwan_killswitches) > 0 || + manager->wwan_interesting); +} + +static gboolean +engine_get_airplane_mode (GsdRfkillManager *manager) +{ + if (!manager->wwan_interesting) + return engine_get_airplane_mode_helper (manager->killswitches); + /* wwan enabled? then airplane mode is off (because an USB modem + could be on in this state) */ + return engine_get_airplane_mode_helper (manager->killswitches) && !manager->wwan_enabled; +} + +static gboolean +engine_get_hardware_airplane_mode (GsdRfkillManager *manager) +{ + GHashTableIter iter; + gpointer key, value; + + /* If we have no killswitches, hw airplane mode is off. */ + if (g_hash_table_size (manager->killswitches) == 0) + return FALSE; + + g_hash_table_iter_init (&iter, manager->killswitches); + while (g_hash_table_iter_next (&iter, &key, &value)) { + int state; + + state = GPOINTER_TO_INT (value); + + /* A single rfkill switch that's not hw blocked? Hw airplane mode is off */ + if (state != RFKILL_STATE_HARD_BLOCKED) { + return FALSE; + } + } + + return TRUE; +} + +static gboolean +engine_get_has_airplane_mode (GsdRfkillManager *manager) +{ + return (g_hash_table_size (manager->killswitches) > 0) || + manager->wwan_interesting; +} + +static gboolean +engine_get_should_show_airplane_mode (GsdRfkillManager *manager) +{ + return (g_strcmp0 (manager->chassis_type, "desktop") != 0) && + (g_strcmp0 (manager->chassis_type, "server") != 0) && + (g_strcmp0 (manager->chassis_type, "vm") != 0) && + (g_strcmp0 (manager->chassis_type, "container") != 0); +} + +static void +engine_properties_changed (GsdRfkillManager *manager) +{ + GVariantBuilder props_builder; + GVariant *props_changed = NULL; + + /* not yet connected to the session bus */ + if (manager->connection == NULL) + return; + + g_variant_builder_init (&props_builder, G_VARIANT_TYPE ("a{sv}")); + + g_variant_builder_add (&props_builder, "{sv}", "AirplaneMode", + g_variant_new_boolean (engine_get_airplane_mode (manager))); + g_variant_builder_add (&props_builder, "{sv}", "HardwareAirplaneMode", + g_variant_new_boolean (engine_get_hardware_airplane_mode (manager))); + g_variant_builder_add (&props_builder, "{sv}", "HasAirplaneMode", + g_variant_new_boolean (engine_get_has_airplane_mode (manager))); + g_variant_builder_add (&props_builder, "{sv}", "ShouldShowAirplaneMode", + g_variant_new_boolean (engine_get_should_show_airplane_mode (manager))); + g_variant_builder_add (&props_builder, "{sv}", "BluetoothAirplaneMode", + g_variant_new_boolean (engine_get_bluetooth_airplane_mode (manager))); + g_variant_builder_add (&props_builder, "{sv}", "BluetoothHardwareAirplaneMode", + g_variant_new_boolean (engine_get_bluetooth_hardware_airplane_mode (manager))); + g_variant_builder_add (&props_builder, "{sv}", "BluetoothHasAirplaneMode", + g_variant_new_boolean (engine_get_has_bluetooth_airplane_mode (manager))); + g_variant_builder_add (&props_builder, "{sv}", "WwanAirplaneMode", + g_variant_new_boolean (engine_get_wwan_airplane_mode (manager))); + g_variant_builder_add (&props_builder, "{sv}", "WwanHardwareAirplaneMode", + g_variant_new_boolean (engine_get_wwan_hardware_airplane_mode (manager))); + g_variant_builder_add (&props_builder, "{sv}", "WwanHasAirplaneMode", + g_variant_new_boolean (engine_get_has_wwan_airplane_mode (manager))); + + props_changed = g_variant_new ("(s@a{sv}@as)", GSD_RFKILL_DBUS_NAME, + g_variant_builder_end (&props_builder), + g_variant_new_strv (NULL, 0)); + + g_dbus_connection_emit_signal (manager->connection, + NULL, + GSD_RFKILL_DBUS_PATH, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + props_changed, NULL); +} + +static void +rfkill_changed (CcRfkillGlib *rfkill, + GList *events, + GsdRfkillManager *manager) +{ + GList *l; + int value; + + for (l = events; l != NULL; l = l->next) { + struct rfkill_event *event = l->data; + const gchar *type = ""; + + if (event->type == RFKILL_TYPE_BLUETOOTH) + type = "Bluetooth "; + else if (event->type == RFKILL_TYPE_WWAN) + type = "WWAN "; + + switch (event->op) { + case RFKILL_OP_ADD: + case RFKILL_OP_CHANGE: + if (event->hard) + value = RFKILL_STATE_HARD_BLOCKED; + else if (event->soft) + value = RFKILL_STATE_SOFT_BLOCKED; + else + value = RFKILL_STATE_UNBLOCKED; + + g_hash_table_insert (manager->killswitches, + GINT_TO_POINTER (event->idx), + GINT_TO_POINTER (value)); + if (event->type == RFKILL_TYPE_BLUETOOTH) + g_hash_table_insert (manager->bt_killswitches, + GINT_TO_POINTER (event->idx), + GINT_TO_POINTER (value)); + else if (event->type == RFKILL_TYPE_WWAN) + g_hash_table_insert (manager->wwan_killswitches, + GINT_TO_POINTER (event->idx), + GINT_TO_POINTER (value)); + g_debug ("%s %srfkill with ID %d", + event->op == RFKILL_OP_ADD ? "Added" : "Changed", + type, event->idx); + break; + case RFKILL_OP_DEL: + g_hash_table_remove (manager->killswitches, + GINT_TO_POINTER (event->idx)); + if (event->type == RFKILL_TYPE_BLUETOOTH) + g_hash_table_remove (manager->bt_killswitches, + GINT_TO_POINTER (event->idx)); + else if (event->type == RFKILL_TYPE_WWAN) + g_hash_table_remove (manager->wwan_killswitches, + GINT_TO_POINTER (event->idx)); + g_debug ("Removed %srfkill with ID %d", type, event->idx); + break; + } + } + + engine_properties_changed (manager); +} + +static void +rfkill_set_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + gboolean ret; + GError *error = NULL; + + ret = cc_rfkill_glib_send_change_all_event_finish (CC_RFKILL_GLIB (source_object), res, &error); + if (!ret) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) + g_debug ("Timed out waiting for blocked rfkills"); + else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to set RFKill: %s", error->message); + g_error_free (error); + } +} + +static void +set_wwan_complete (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GError *error; + GVariant *variant; + + error = NULL; + variant = g_dbus_proxy_call_finish (G_DBUS_PROXY (object), result, &error); + + if (variant == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to set WWAN power status: %s", error->message); + + g_error_free (error); + } else { + g_variant_unref (variant); + } +} + +static gboolean +engine_set_bluetooth_airplane_mode (GsdRfkillManager *manager, + gboolean enable) +{ + cc_rfkill_glib_send_change_all_event (manager->rfkill, RFKILL_TYPE_BLUETOOTH, + enable, manager->cancellable, rfkill_set_cb, manager); + + return TRUE; +} + +static gboolean +engine_set_wwan_airplane_mode (GsdRfkillManager *manager, + gboolean enable) +{ + cc_rfkill_glib_send_change_all_event (manager->rfkill, RFKILL_TYPE_WWAN, + enable, manager->cancellable, rfkill_set_cb, manager); + + if (manager->nm_client) { + g_dbus_proxy_call (manager->nm_client, + "org.freedesktop.DBus.Properties.Set", + g_variant_new ("(ssv)", + "org.freedesktop.NetworkManager", + "WwanEnabled", + g_variant_new_boolean (!enable)), + G_DBUS_CALL_FLAGS_NONE, + -1, /* timeout */ + manager->cancellable, + set_wwan_complete, NULL); + } + + return TRUE; +} + +static gboolean +engine_set_airplane_mode (GsdRfkillManager *manager, + gboolean enable) +{ + cc_rfkill_glib_send_change_all_event (manager->rfkill, RFKILL_TYPE_ALL, + enable, manager->cancellable, rfkill_set_cb, manager); + + /* Note: we set the the NM property even if there are no modems, so we don't + need to resync when one is plugged in */ + if (manager->nm_client) { + g_dbus_proxy_call (manager->nm_client, + "org.freedesktop.DBus.Properties.Set", + g_variant_new ("(ssv)", + "org.freedesktop.NetworkManager", + "WwanEnabled", + g_variant_new_boolean (!enable)), + G_DBUS_CALL_FLAGS_NONE, + -1, /* timeout */ + manager->cancellable, + set_wwan_complete, NULL); + } + + return TRUE; +} + +static gboolean +handle_set_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GVariant *value, + GError **error, + gpointer user_data) +{ + GsdRfkillManager *manager = GSD_RFKILL_MANAGER (user_data); + + if (g_strcmp0 (property_name, "AirplaneMode") == 0) { + gboolean airplane_mode; + g_variant_get (value, "b", &airplane_mode); + return engine_set_airplane_mode (manager, airplane_mode); + } else if (g_strcmp0 (property_name, "BluetoothAirplaneMode") == 0) { + gboolean airplane_mode; + g_variant_get (value, "b", &airplane_mode); + return engine_set_bluetooth_airplane_mode (manager, airplane_mode); + } + + if (g_strcmp0 (property_name, "WwanAirplaneMode") == 0) { + gboolean airplane_mode; + g_variant_get (value, "b", &airplane_mode); + return engine_set_wwan_airplane_mode (manager, airplane_mode); + } + + return FALSE; +} + +static GVariant * +handle_get_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, + gpointer user_data) +{ + GsdRfkillManager *manager = GSD_RFKILL_MANAGER (user_data); + + /* Check session pointer as a proxy for whether the manager is in the + start or stop state */ + if (manager->connection == NULL) { + return NULL; + } + + if (g_strcmp0 (property_name, "AirplaneMode") == 0) { + gboolean airplane_mode; + airplane_mode = engine_get_airplane_mode (manager); + return g_variant_new_boolean (airplane_mode); + } + + if (g_strcmp0 (property_name, "HardwareAirplaneMode") == 0) { + gboolean hw_airplane_mode; + hw_airplane_mode = engine_get_hardware_airplane_mode (manager); + return g_variant_new_boolean (hw_airplane_mode); + } + + if (g_strcmp0 (property_name, "ShouldShowAirplaneMode") == 0) { + gboolean should_show_airplane_mode; + should_show_airplane_mode = engine_get_should_show_airplane_mode (manager); + return g_variant_new_boolean (should_show_airplane_mode); + } + + if (g_strcmp0 (property_name, "HasAirplaneMode") == 0) { + gboolean has_airplane_mode; + has_airplane_mode = engine_get_has_airplane_mode (manager); + return g_variant_new_boolean (has_airplane_mode); + } + + if (g_strcmp0 (property_name, "BluetoothAirplaneMode") == 0) { + gboolean airplane_mode; + airplane_mode = engine_get_bluetooth_airplane_mode (manager); + return g_variant_new_boolean (airplane_mode); + } + + if (g_strcmp0 (property_name, "BluetoothHardwareAirplaneMode") == 0) { + gboolean hw_airplane_mode; + hw_airplane_mode = engine_get_bluetooth_hardware_airplane_mode (manager); + return g_variant_new_boolean (hw_airplane_mode); + } + + if (g_strcmp0 (property_name, "BluetoothHasAirplaneMode") == 0) { + gboolean has_airplane_mode; + has_airplane_mode = engine_get_has_bluetooth_airplane_mode (manager); + return g_variant_new_boolean (has_airplane_mode); + } + + if (g_strcmp0 (property_name, "WwanAirplaneMode") == 0) { + gboolean airplane_mode; + airplane_mode = engine_get_wwan_airplane_mode (manager); + return g_variant_new_boolean (airplane_mode); + } + + if (g_strcmp0 (property_name, "WwanHardwareAirplaneMode") == 0) { + gboolean hw_airplane_mode; + hw_airplane_mode = engine_get_wwan_hardware_airplane_mode (manager); + return g_variant_new_boolean (hw_airplane_mode); + } + + if (g_strcmp0 (property_name, "WwanHasAirplaneMode") == 0) { + gboolean has_airplane_mode; + has_airplane_mode = engine_get_has_wwan_airplane_mode (manager); + return g_variant_new_boolean (has_airplane_mode); + } + + return NULL; +} + +static const GDBusInterfaceVTable interface_vtable = +{ + NULL, + handle_get_property, + handle_set_property +}; + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *res, + GsdRfkillManager *manager) +{ + GDBusConnection *connection; + GError *error = NULL; + + connection = g_bus_get_finish (res, &error); + if (connection == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Could not get session bus: %s", error->message); + g_error_free (error); + return; + } + manager->connection = connection; + + g_dbus_connection_register_object (connection, + GSD_RFKILL_DBUS_PATH, + manager->introspection_data->interfaces[0], + &interface_vtable, + manager, + NULL, + NULL); + + manager->name_id = g_bus_own_name_on_connection (connection, + GSD_RFKILL_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, + NULL, + NULL, + NULL); + + manager->session = gnome_settings_bus_get_session_proxy (); + manager->rfkill_input_inhibit_binding = g_object_bind_property (manager->session, "session-is-active", + manager->rfkill, "rfkill-input-inhibited", + G_BINDING_SYNC_CREATE); +} + +static void +sync_wwan_enabled (GsdRfkillManager *manager) +{ + GVariant *property; + + property = g_dbus_proxy_get_cached_property (manager->nm_client, + "WwanEnabled"); + + if (property == NULL) { + /* GDBus telling us NM went down */ + return; + } + + manager->wwan_enabled = g_variant_get_boolean (property); + engine_properties_changed (manager); + + g_variant_unref (property); +} + +static void +nm_signal (GDBusProxy *proxy, + char *sender_name, + char *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GsdRfkillManager *manager = user_data; + GVariant *changed; + GVariant *property; + + if (g_strcmp0 (signal_name, "PropertiesChanged") == 0) { + changed = g_variant_get_child_value (parameters, 0); + property = g_variant_lookup_value (changed, "WwanEnabled", G_VARIANT_TYPE ("b")); + g_dbus_proxy_set_cached_property (proxy, "WwanEnabled", property); + + if (property != NULL) { + sync_wwan_enabled (manager); + g_variant_unref (property); + } + + g_variant_unref (changed); + } +} + +static void +on_nm_proxy_gotten (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GsdRfkillManager *manager = user_data; + GDBusProxy *proxy; + GError *error; + + error = NULL; + proxy = g_dbus_proxy_new_for_bus_finish (result, &error); + + if (proxy == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) + g_warning ("Failed to acquire NetworkManager proxy: %s", error->message); + + g_error_free (error); + goto out; + } + + manager->nm_client = proxy; + + g_signal_connect (manager->nm_client, "g-signal", + G_CALLBACK (nm_signal), manager); + sync_wwan_enabled (manager); + + out: + g_object_unref (manager); +} + +static void +sync_wwan_interesting (GDBusObjectManager *object_manager, + GDBusObject *object, + GDBusInterface *interface, + gpointer user_data) +{ + GsdRfkillManager *manager = user_data; + GList *objects; + + objects = g_dbus_object_manager_get_objects (object_manager); + manager->wwan_interesting = (objects != NULL); + engine_properties_changed (manager); + + g_list_free_full (objects, g_object_unref); +} + +static void +on_mm_proxy_gotten (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GsdRfkillManager *manager = user_data; + GDBusObjectManager *proxy; + GError *error; + + error = NULL; + proxy = g_dbus_object_manager_client_new_for_bus_finish (result, &error); + + if (proxy == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) + g_warning ("Failed to acquire ModemManager proxy: %s", error->message); + + g_error_free (error); + goto out; + } + + manager->mm_client = proxy; + + g_signal_connect (manager->mm_client, "interface-added", + G_CALLBACK (sync_wwan_interesting), manager); + g_signal_connect (manager->mm_client, "interface-removed", + G_CALLBACK (sync_wwan_interesting), manager); + sync_wwan_interesting (manager->mm_client, NULL, NULL, manager); + + out: + g_object_unref (manager); +} + +gboolean +gsd_rfkill_manager_start (GsdRfkillManager *manager, + GError **error) +{ + g_autoptr(GError) local_error = NULL; + + gnome_settings_profile_start (NULL); + + manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + g_assert (manager->introspection_data != NULL); + + manager->killswitches = g_hash_table_new (g_direct_hash, g_direct_equal); + manager->bt_killswitches = g_hash_table_new (g_direct_hash, g_direct_equal); + manager->wwan_killswitches = g_hash_table_new (g_direct_hash, g_direct_equal); + manager->rfkill = cc_rfkill_glib_new (); + g_signal_connect (G_OBJECT (manager->rfkill), "changed", + G_CALLBACK (rfkill_changed), manager); + + if (!cc_rfkill_glib_open (manager->rfkill, &local_error)) { + g_warning ("Error setting up rfkill: %s", local_error->message); + g_clear_error (&local_error); + } + + manager->cancellable = g_cancellable_new (); + + manager->chassis_type = gnome_settings_get_chassis_type (); + + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, /* g-interface-info */ + "org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager", + "org.freedesktop.NetworkManager", + manager->cancellable, + on_nm_proxy_gotten, g_object_ref (manager)); + + g_dbus_object_manager_client_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, + "org.freedesktop.ModemManager1", + "/org/freedesktop/ModemManager1", + NULL, NULL, NULL, /* get_proxy_type and closure */ + manager->cancellable, + on_mm_proxy_gotten, g_object_ref (manager)); + + /* Start process of owning a D-Bus name */ + g_bus_get (G_BUS_TYPE_SESSION, + manager->cancellable, + (GAsyncReadyCallback) on_bus_gotten, + manager); + + gnome_settings_profile_end (NULL); + + return TRUE; +} + +void +gsd_rfkill_manager_stop (GsdRfkillManager *manager) +{ + g_debug ("Stopping rfkill manager"); + + if (manager->name_id != 0) { + g_bus_unown_name (manager->name_id); + manager->name_id = 0; + } + + g_clear_pointer (&manager->introspection_data, g_dbus_node_info_unref); + g_clear_object (&manager->connection); + g_clear_object (&manager->rfkill_input_inhibit_binding); + g_clear_object (&manager->session); + g_clear_object (&manager->rfkill); + g_clear_pointer (&manager->killswitches, g_hash_table_destroy); + g_clear_pointer (&manager->bt_killswitches, g_hash_table_destroy); + g_clear_pointer (&manager->wwan_killswitches, g_hash_table_destroy); + + if (manager->cancellable) { + g_cancellable_cancel (manager->cancellable); + g_clear_object (&manager->cancellable); + } + + g_clear_object (&manager->nm_client); + g_clear_object (&manager->mm_client); + manager->wwan_enabled = FALSE; + manager->wwan_interesting = FALSE; + + g_clear_pointer (&manager->chassis_type, g_free); +} + +static void +gsd_rfkill_manager_finalize (GObject *object) +{ + GsdRfkillManager *manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_RFKILL_MANAGER (object)); + + manager = GSD_RFKILL_MANAGER (object); + + g_return_if_fail (manager != NULL); + + gsd_rfkill_manager_stop (manager); + + G_OBJECT_CLASS (gsd_rfkill_manager_parent_class)->finalize (object); +} + +GsdRfkillManager * +gsd_rfkill_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_RFKILL_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_RFKILL_MANAGER (manager_object); +} diff --git a/plugins/rfkill/gsd-rfkill-manager.h b/plugins/rfkill/gsd-rfkill-manager.h new file mode 100644 index 0000000..025a7d8 --- /dev/null +++ b/plugins/rfkill/gsd-rfkill-manager.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_RFKILL_MANAGER_H +#define __GSD_RFKILL_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_RFKILL_MANAGER (gsd_rfkill_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdRfkillManager, gsd_rfkill_manager, GSD, RFKILL_MANAGER, GObject) + +GsdRfkillManager * gsd_rfkill_manager_new (void); +gboolean gsd_rfkill_manager_start (GsdRfkillManager *manager, + GError **error); +void gsd_rfkill_manager_stop (GsdRfkillManager *manager); + +G_END_DECLS + +#endif /* __GSD_RFKILL_MANAGER_H */ diff --git a/plugins/rfkill/main.c b/plugins/rfkill/main.c new file mode 100644 index 0000000..4f19f5d --- /dev/null +++ b/plugins/rfkill/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_rfkill_manager_new +#define START gsd_rfkill_manager_start +#define STOP gsd_rfkill_manager_stop +#define MANAGER GsdRfkillManager +#include "gsd-rfkill-manager.h" + +#include "daemon-skeleton.h" diff --git a/plugins/rfkill/meson.build b/plugins/rfkill/meson.build new file mode 100644 index 0000000..4d70352 --- /dev/null +++ b/plugins/rfkill/meson.build @@ -0,0 +1,28 @@ +install_data( + '61-gnome-settings-daemon-rfkill.rules', + install_dir: join_paths(udev_dir, 'rules.d') +) + +sources = files( + 'gsd-rfkill-manager.c', + 'rfkill-glib.c', + 'main.c' +) + +deps = plugins_deps +deps += [ + gio_unix_dep, + gudev_dep, + m_dep +] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) diff --git a/plugins/rfkill/rfkill-glib.c b/plugins/rfkill/rfkill-glib.c new file mode 100644 index 0000000..22309dc --- /dev/null +++ b/plugins/rfkill/rfkill-glib.c @@ -0,0 +1,680 @@ +/* + * + * gnome-bluetooth - Bluetooth integration for GNOME + * + * Copyright (C) 2012 Bastien Nocera <hadess@hadess.net> + * Copyright © 2017 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <errno.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <string.h> +#include <sys/ioctl.h> + +#include <glib.h> +#include <gio/gio.h> +#include <gio/gunixoutputstream.h> + +#include "rfkill-glib.h" +#include <gudev/gudev.h> + +enum { + CHANGED, + LAST_SIGNAL +}; + +enum { + PROP_RFKILL_INPUT_INHIBITED = 1 +}; + +static int signals[LAST_SIGNAL] = { 0 }; + +struct _CcRfkillGlib { + GObject parent; + + GUdevClient *udev; + gchar *device_file; + + GOutputStream *stream; + GIOChannel *channel; + guint watch_id; + + /* rfkill-input inhibitor */ + gboolean noinput; + int noinput_fd; + + /* Pending Bluetooth enablement. + * If (@change_all_timeout_id != 0), then (task != NULL). The converse + * does not necessarily hold. */ + guint change_all_timeout_id; + GTask *task; +}; + +G_DEFINE_TYPE (CcRfkillGlib, cc_rfkill_glib, G_TYPE_OBJECT) + +#define CHANGE_ALL_TIMEOUT 500 + +static const char *type_to_string (unsigned int type); + +static void +cancel_current_task (CcRfkillGlib *rfkill) +{ + if (rfkill->task != NULL) { + g_cancellable_cancel (g_task_get_cancellable (rfkill->task)); + g_clear_object (&rfkill->task); + } + + if (rfkill->change_all_timeout_id != 0) { + g_source_remove (rfkill->change_all_timeout_id); + rfkill->change_all_timeout_id = 0; + } +} + +/* Note that this can return %FALSE without setting @error. */ +gboolean +cc_rfkill_glib_send_change_all_event_finish (CcRfkillGlib *rfkill, + GAsyncResult *res, + GError **error) +{ + g_return_val_if_fail (CC_RFKILL_IS_GLIB (rfkill), FALSE); + g_return_val_if_fail (g_task_is_valid (res, rfkill), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (res, cc_rfkill_glib_send_change_all_event), FALSE); + + return g_task_propagate_boolean (G_TASK (res), error); +} + +static void +write_change_all_again_done_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + CcRfkillGlib *rfkill = g_task_get_source_object (task); + g_autoptr(GError) error = NULL; + gssize ret; + + g_debug ("Finished writing second RFKILL_OP_CHANGE_ALL event"); + + ret = g_output_stream_write_finish (G_OUTPUT_STREAM (source_object), res, &error); + if (ret < 0) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_boolean (task, ret >= 0); + + /* If this @task has been cancelled, it may have been superceded. */ + if (rfkill->task == task) + g_clear_object (&rfkill->task); +} + +static gboolean +write_change_all_timeout_cb (CcRfkillGlib *rfkill) +{ + struct rfkill_event *event; + + g_assert (rfkill->task != NULL); + + g_debug ("Sending second RFKILL_OP_CHANGE_ALL timed out"); + + event = g_task_get_task_data (rfkill->task); + g_task_return_new_error (rfkill->task, + G_IO_ERROR, G_IO_ERROR_TIMED_OUT, + "Enabling rfkill for %s timed out", + type_to_string (event->type)); + + g_clear_object (&rfkill->task); + rfkill->change_all_timeout_id = 0; + + return G_SOURCE_REMOVE; +} + +static void +write_change_all_done_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + CcRfkillGlib *rfkill = g_task_get_source_object (task); + g_autoptr(GError) error = NULL; + gssize ret; + struct rfkill_event *event; + + g_debug ("Sending original RFKILL_OP_CHANGE_ALL event done"); + + event = g_task_get_task_data (task); + ret = g_output_stream_write_finish (G_OUTPUT_STREAM (source_object), res, &error); + if (ret < 0) { + g_task_return_error (task, g_steal_pointer (&error)); + goto bail; + } else if (event->soft == 1 || + event->type != RFKILL_TYPE_BLUETOOTH) { + g_task_return_boolean (task, ret >= 0); + goto bail; + } + + g_assert (rfkill->change_all_timeout_id == 0); + rfkill->change_all_timeout_id = g_timeout_add (CHANGE_ALL_TIMEOUT, + (GSourceFunc) write_change_all_timeout_cb, + rfkill); + + return; + +bail: + /* If this @task has been cancelled, it may have been superceded. */ + if (rfkill->task == task) + g_clear_object (&rfkill->task); +} + +void +cc_rfkill_glib_send_change_all_event (CcRfkillGlib *rfkill, + guint rfkill_type, + gboolean enable, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + struct rfkill_event *event; + g_autoptr(GCancellable) task_cancellable = NULL; + + g_return_if_fail (CC_RFKILL_IS_GLIB (rfkill)); + g_return_if_fail (rfkill->stream); + + task_cancellable = g_cancellable_new (); + g_signal_connect_object (cancellable, "cancelled", + (GCallback) g_cancellable_cancel, + task_cancellable, + G_CONNECT_SWAPPED); + /* Now check if it is cancelled already */ + if (g_cancellable_is_cancelled (cancellable)) + g_cancellable_cancel (task_cancellable); + + task = g_task_new (rfkill, task_cancellable, callback, user_data); + g_task_set_source_tag (task, cc_rfkill_glib_send_change_all_event); + + /* Clear any previous task. */ + cancel_current_task (rfkill); + g_assert (rfkill->task == NULL); + + /* Start writing out a new event. */ + event = g_new0 (struct rfkill_event, 1); + event->op = RFKILL_OP_CHANGE_ALL; + event->type = rfkill_type; + event->soft = enable ? 1 : 0; + + g_task_set_task_data (task, event, g_free); + rfkill->task = g_object_ref (task); + rfkill->change_all_timeout_id = 0; + + g_output_stream_write_async (rfkill->stream, + event, sizeof(struct rfkill_event), + G_PRIORITY_DEFAULT, + task_cancellable, write_change_all_done_cb, + g_object_ref (task)); +} + +static const char * +type_to_string (unsigned int type) +{ + switch (type) { + case RFKILL_TYPE_ALL: + return "ALL"; + case RFKILL_TYPE_WLAN: + return "WLAN"; + case RFKILL_TYPE_BLUETOOTH: + return "BLUETOOTH"; + case RFKILL_TYPE_UWB: + return "UWB"; + case RFKILL_TYPE_WIMAX: + return "WIMAX"; + case RFKILL_TYPE_WWAN: + return "WWAN"; + default: + return "UNKNOWN"; + } +} + +static const char * +op_to_string (unsigned int op) +{ + switch (op) { + case RFKILL_OP_ADD: + return "ADD"; + case RFKILL_OP_DEL: + return "DEL"; + case RFKILL_OP_CHANGE: + return "CHANGE"; + case RFKILL_OP_CHANGE_ALL: + return "CHANGE_ALL"; + default: + g_assert_not_reached (); + } +} + +static void +print_event (struct rfkill_event *event) +{ + g_debug ("RFKILL event: idx %u type %u (%s) op %u (%s) soft %u hard %u", + event->idx, + event->type, type_to_string (event->type), + event->op, op_to_string (event->op), + event->soft, event->hard); +} + +static gboolean +got_change_event (GList *events) +{ + GList *l; + + g_assert (events != NULL); + + for (l = events ; l != NULL; l = l->next) { + struct rfkill_event *event = l->data; + + if (event->op == RFKILL_OP_CHANGE) + return TRUE; + } + + return FALSE; +} + +static void +emit_changed_signal_and_free (CcRfkillGlib *rfkill, + GList *events) +{ + if (events == NULL) + return; + + g_signal_emit (G_OBJECT (rfkill), + signals[CHANGED], + 0, events); + + if (rfkill->change_all_timeout_id > 0 && + got_change_event (events)) { + struct rfkill_event *event; + + g_debug ("Received a change event after a RFKILL_OP_CHANGE_ALL event, re-sending RFKILL_OP_CHANGE_ALL"); + + event = g_task_get_task_data (rfkill->task); + g_output_stream_write_async (rfkill->stream, + event, sizeof(struct rfkill_event), + G_PRIORITY_DEFAULT, + g_task_get_cancellable (rfkill->task), + write_change_all_again_done_cb, + g_object_ref (rfkill->task)); + + g_source_remove (rfkill->change_all_timeout_id); + rfkill->change_all_timeout_id = 0; + } + + g_list_free_full (events, g_free); +} + +static gboolean +event_cb (GIOChannel *source, + GIOCondition condition, + CcRfkillGlib *rfkill) +{ + GList *events; + + events = NULL; + + if (condition & G_IO_IN) { + GIOStatus status; + struct rfkill_event event; + gsize read; + + status = g_io_channel_read_chars (source, + (char *) &event, + sizeof(event), + &read, + NULL); + + while (status == G_IO_STATUS_NORMAL && read == sizeof(event)) { + struct rfkill_event *event_ptr; + + print_event (&event); + + event_ptr = g_memdup (&event, sizeof(event)); + events = g_list_prepend (events, event_ptr); + + status = g_io_channel_read_chars (source, + (char *) &event, + sizeof(event), + &read, + NULL); + } + events = g_list_reverse (events); + } else { + g_debug ("Something unexpected happened on rfkill fd"); + return FALSE; + } + + emit_changed_signal_and_free (rfkill, events); + + return TRUE; +} + +static void +cc_rfkill_glib_init (CcRfkillGlib *rfkill) +{ + rfkill->device_file = NULL; + rfkill->noinput_fd = -1; +} + +static gboolean +_cc_rfkill_glib_open (CcRfkillGlib *rfkill, + GError **error) +{ + int fd; + int ret; + GList *events; + + g_return_val_if_fail (CC_RFKILL_IS_GLIB (rfkill), FALSE); + g_return_val_if_fail (rfkill->stream == NULL, FALSE); + g_assert (rfkill->device_file); + + fd = open (rfkill->device_file, O_RDWR); + + if (fd < 0) { + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Could not open RFKILL control device, please verify your installation"); + return FALSE; + } + + ret = fcntl(fd, F_SETFL, O_NONBLOCK); + if (ret < 0) { + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (errno), + "Can't set RFKILL control device to non-blocking"); + close(fd); + return FALSE; + } + + events = NULL; + + while (1) { + struct rfkill_event event; + struct rfkill_event *event_ptr; + ssize_t len; + + len = read(fd, &event, sizeof(event)); + if (len < 0) { + if (errno == EAGAIN) + break; + g_debug ("Reading of RFKILL events failed"); + break; + } + + if (len != RFKILL_EVENT_SIZE_V1) { + g_warning ("Wrong size of RFKILL event\n"); + continue; + } + + if (event.op != RFKILL_OP_ADD) + continue; + + g_debug ("Read killswitch of type '%s' (idx=%d): soft %d hard %d", + type_to_string (event.type), + event.idx, event.soft, event.hard); + + event_ptr = g_memdup (&event, sizeof(event)); + events = g_list_prepend (events, event_ptr); + } + + /* Setup monitoring */ + rfkill->channel = g_io_channel_unix_new (fd); + g_io_channel_set_encoding (rfkill->channel, NULL, NULL); + g_io_channel_set_buffered (rfkill->channel, FALSE); + rfkill->watch_id = g_io_add_watch (rfkill->channel, + G_IO_IN | G_IO_HUP | G_IO_ERR, + (GIOFunc) event_cb, + rfkill); + + if (events) { + events = g_list_reverse (events); + emit_changed_signal_and_free (rfkill, events); + } else { + g_debug ("No rfkill device available on startup"); + } + + /* Setup write stream */ + rfkill->stream = g_unix_output_stream_new (fd, TRUE); + + return TRUE; +} + +static void +uevent_cb (GUdevClient *client, + gchar *action, + GUdevDevice *device, + gpointer user_data) +{ + CcRfkillGlib *rfkill = CC_RFKILL_GLIB (user_data); + + if (g_strcmp0 (action, "add") != 0) + return; + + if (g_strcmp0 (g_udev_device_get_name (device), "rfkill") == 0) { + g_autoptr(GError) error = NULL; + + g_debug ("Rfkill device has been created"); + + if (g_udev_device_get_device_file (device)) { + g_clear_pointer (&rfkill->device_file, g_free); + rfkill->device_file = g_strdup (g_udev_device_get_device_file (device)); + } else { + g_warning ("rfkill udev device does not have a device file!"); + } + + if (!_cc_rfkill_glib_open (rfkill, &error)) + g_warning ("Could not open rfkill device: %s", error->message); + else + g_debug ("Opened rfkill device after uevent"); + + g_clear_object (&rfkill->udev); + + /* Sync rfkill input inhibition state*/ + cc_rfkill_glib_set_rfkill_input_inhibited (rfkill, rfkill->noinput); + } +} + +gboolean +cc_rfkill_glib_open (CcRfkillGlib *rfkill, + GError **error) +{ + const char * const subsystems[] = { "misc", NULL }; + GUdevDevice *device; + + rfkill->udev = g_udev_client_new (subsystems); + g_debug ("Setting up uevent listener"); + g_signal_connect (rfkill->udev, "uevent", G_CALLBACK (uevent_cb), rfkill); + + /* Simulate uevent if device already exists. */ + device = g_udev_client_query_by_subsystem_and_name (rfkill->udev, "misc", "rfkill"); + if (device) + uevent_cb (rfkill->udev, "add", device, rfkill); + + return TRUE; +} + +#define RFKILL_INPUT_INHIBITED(rfkill) (rfkill->noinput_fd >= 0) + +gboolean +cc_rfkill_glib_get_rfkill_input_inhibited (CcRfkillGlib *rfkill) +{ + g_return_val_if_fail (CC_RFKILL_IS_GLIB (rfkill), FALSE); + + return rfkill->noinput; +} + +void +cc_rfkill_glib_set_rfkill_input_inhibited (CcRfkillGlib *rfkill, + gboolean inhibit) +{ + g_return_if_fail (CC_RFKILL_IS_GLIB (rfkill)); + + /* Shortcut in case we don't have an rfkill device */ + if (!rfkill->stream) { + if (rfkill->noinput == inhibit) + return; + + rfkill->noinput = inhibit; + g_object_notify (G_OBJECT (rfkill), "rfkill-input-inhibited"); + + return; + } + + if (!inhibit && RFKILL_INPUT_INHIBITED(rfkill)) { + close (rfkill->noinput_fd); + g_debug ("Closed rfkill noinput FD."); + + rfkill->noinput_fd = -1; + } + + if (inhibit && !RFKILL_INPUT_INHIBITED(rfkill)) { + int fd, res; + /* Open write only as we don't want to do any IO to it ever. */ + fd = open (rfkill->device_file, O_WRONLY); + if (fd < 0) { + if (errno == EACCES) + g_warning ("Could not open RFKILL control device, please verify your installation"); + else + g_debug ("Could not open RFKILL control device: %s", g_strerror (errno)); + return; + } + + res = ioctl (fd, RFKILL_IOCTL_NOINPUT, (long) 0); + if (res != 0) { + g_warning ("Could not disable kernel handling of RFKILL related keys: %s", g_strerror (errno)); + close (fd); + return; + } + + g_debug ("Opened rfkill-input inhibitor."); + + rfkill->noinput_fd = fd; + } + + if (rfkill->noinput != RFKILL_INPUT_INHIBITED(rfkill)) { + rfkill->noinput = RFKILL_INPUT_INHIBITED(rfkill); + g_object_notify (G_OBJECT (rfkill), "rfkill-input-inhibited"); + } +} + +static void +cc_rfkill_glib_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CcRfkillGlib *rfkill = CC_RFKILL_GLIB (object); + + switch (prop_id) { + case PROP_RFKILL_INPUT_INHIBITED: + cc_rfkill_glib_set_rfkill_input_inhibited (rfkill, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +cc_rfkill_glib_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcRfkillGlib *rfkill = CC_RFKILL_GLIB (object); + + switch (prop_id) { + case PROP_RFKILL_INPUT_INHIBITED: + g_value_set_boolean (value, rfkill->noinput); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +cc_rfkill_glib_finalize (GObject *object) +{ + CcRfkillGlib *rfkill = CC_RFKILL_GLIB (object); + + cancel_current_task (rfkill); + + /* cleanup monitoring */ + if (rfkill->watch_id > 0) { + g_source_remove (rfkill->watch_id); + rfkill->watch_id = 0; + g_io_channel_shutdown (rfkill->channel, FALSE, NULL); + g_io_channel_unref (rfkill->channel); + } + g_clear_object (&rfkill->stream); + + if (RFKILL_INPUT_INHIBITED(rfkill)) { + close (rfkill->noinput_fd); + rfkill->noinput_fd = -1; + } + + g_clear_pointer (&rfkill->device_file, g_free); + g_clear_object (&rfkill->udev); + + G_OBJECT_CLASS(cc_rfkill_glib_parent_class)->finalize(object); +} + +static void +cc_rfkill_glib_class_init(CcRfkillGlibClass *klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + + object_class->set_property = cc_rfkill_glib_set_property; + object_class->get_property = cc_rfkill_glib_get_property; + object_class->finalize = cc_rfkill_glib_finalize; + + g_object_class_install_property (object_class, + PROP_RFKILL_INPUT_INHIBITED, + g_param_spec_boolean ("rfkill-input-inhibited", + "Rfkill input inhibited", + "Whether to prevent the kernel from handling RFKILL related key events.", + FALSE, + G_PARAM_READWRITE)); + + signals[CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 1, G_TYPE_POINTER); + +} + +CcRfkillGlib * +cc_rfkill_glib_new (void) +{ + return CC_RFKILL_GLIB (g_object_new (CC_RFKILL_TYPE_GLIB, NULL)); +} diff --git a/plugins/rfkill/rfkill-glib.h b/plugins/rfkill/rfkill-glib.h new file mode 100644 index 0000000..0655eb4 --- /dev/null +++ b/plugins/rfkill/rfkill-glib.h @@ -0,0 +1,57 @@ +/* + * + * gnome-bluetooth - Bluetooth integration for GNOME + * + * Copyright (C) 2012 Bastien Nocera <hadess@hadess.net> + * Copyright © 2017 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __CC_RFKILL_GLIB_H +#define __CC_RFKILL_GLIB_H + +#include <glib-object.h> +#include <gio/gio.h> +#include "rfkill.h" + +G_BEGIN_DECLS + +#define CC_RFKILL_TYPE_GLIB cc_rfkill_glib_get_type () +G_DECLARE_FINAL_TYPE (CcRfkillGlib, cc_rfkill_glib, CC_RFKILL, GLIB, GObject) + +CcRfkillGlib *cc_rfkill_glib_new (void); +gboolean cc_rfkill_glib_open (CcRfkillGlib *rfkill, + GError **error); + +gboolean cc_rfkill_glib_get_rfkill_input_inhibited (CcRfkillGlib *rfkill); +void cc_rfkill_glib_set_rfkill_input_inhibited (CcRfkillGlib *rfkill, + gboolean noinput); + +void cc_rfkill_glib_send_change_all_event (CcRfkillGlib *rfkill, + guint rfkill_type, + gboolean enable, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean cc_rfkill_glib_send_change_all_event_finish (CcRfkillGlib *rfkill, + GAsyncResult *res, + GError **error); + +G_END_DECLS + +#endif /* __CC_RFKILL_GLIB_H */ diff --git a/plugins/rfkill/rfkill.h b/plugins/rfkill/rfkill.h new file mode 100644 index 0000000..abb2c66 --- /dev/null +++ b/plugins/rfkill/rfkill.h @@ -0,0 +1,107 @@ +#ifndef __RFKILL_H +#define __RFKILL_H + +/* + * Copyright (C) 2006 - 2007 Ivo van Doorn + * Copyright (C) 2007 Dmitry Torokhov + * Copyright 2009 Johannes Berg <johannes@sipsolutions.net> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <linux/types.h> + +/* define userspace visible states */ +#define RFKILL_STATE_SOFT_BLOCKED 0 +#define RFKILL_STATE_UNBLOCKED 1 +#define RFKILL_STATE_HARD_BLOCKED 2 + +/** + * enum rfkill_type - type of rfkill switch. + * + * @RFKILL_TYPE_ALL: toggles all switches (requests only - not a switch type) + * @RFKILL_TYPE_WLAN: switch is on a 802.11 wireless network device. + * @RFKILL_TYPE_BLUETOOTH: switch is on a bluetooth device. + * @RFKILL_TYPE_UWB: switch is on a ultra wideband device. + * @RFKILL_TYPE_WIMAX: switch is on a WiMAX device. + * @RFKILL_TYPE_WWAN: switch is on a wireless WAN device. + * @RFKILL_TYPE_GPS: switch is on a GPS device. + * @RFKILL_TYPE_FM: switch is on a FM radio device. + * @NUM_RFKILL_TYPES: number of defined rfkill types + */ +enum rfkill_type { + RFKILL_TYPE_ALL = 0, + RFKILL_TYPE_WLAN, + RFKILL_TYPE_BLUETOOTH, + RFKILL_TYPE_UWB, + RFKILL_TYPE_WIMAX, + RFKILL_TYPE_WWAN, + RFKILL_TYPE_GPS, + RFKILL_TYPE_FM, + NUM_RFKILL_TYPES, +}; + +/** + * enum rfkill_operation - operation types + * @RFKILL_OP_ADD: a device was added + * @RFKILL_OP_DEL: a device was removed + * @RFKILL_OP_CHANGE: a device's state changed -- userspace changes one device + * @RFKILL_OP_CHANGE_ALL: userspace changes all devices (of a type, or all) + */ +enum rfkill_operation { + RFKILL_OP_ADD = 0, + RFKILL_OP_DEL, + RFKILL_OP_CHANGE, + RFKILL_OP_CHANGE_ALL, +}; + +/** + * struct rfkill_event - events for userspace on /dev/rfkill + * @idx: index of dev rfkill + * @type: type of the rfkill struct + * @op: operation code + * @hard: hard state (0/1) + * @soft: soft state (0/1) + * + * Structure used for userspace communication on /dev/rfkill, + * used for events from the kernel and control to the kernel. + */ +struct rfkill_event { + __u32 idx; + __u8 type; + __u8 op; + __u8 soft, hard; +} __attribute__((packed)); + +/* + * We are planning to be backward and forward compatible with changes + * to the event struct, by adding new, optional, members at the end. + * When reading an event (whether the kernel from userspace or vice + * versa) we need to accept anything that's at least as large as the + * version 1 event size, but might be able to accept other sizes in + * the future. + * + * One exception is the kernel -- we already have two event sizes in + * that we've made the 'hard' member optional since our only option + * is to ignore it anyway. + */ +#define RFKILL_EVENT_SIZE_V1 8 + +/* ioctl for turning off rfkill-input (if present) */ +#define RFKILL_IOC_MAGIC 'R' +#define RFKILL_IOC_NOINPUT 1 +#define RFKILL_IOCTL_NOINPUT _IO(RFKILL_IOC_MAGIC, RFKILL_IOC_NOINPUT) + +/* and that's all userspace gets */ + +#endif /* RFKILL_H */ diff --git a/plugins/screensaver-proxy/gsd-screensaver-proxy-manager.c b/plugins/screensaver-proxy/gsd-screensaver-proxy-manager.c new file mode 100644 index 0000000..e434eb9 --- /dev/null +++ b/plugins/screensaver-proxy/gsd-screensaver-proxy-manager.c @@ -0,0 +1,444 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2012 Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <sys/types.h> +#include <sys/wait.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> + +#include <locale.h> + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include "gnome-settings-bus.h" +#include "gnome-settings-profile.h" +#include "gsd-screensaver-proxy-manager.h" + +/* As available in: + * https://projects.kde.org/projects/kde/kde-workspace/repository/revisions/master/entry/ksmserver/screenlocker/dbus/org.freedesktop.ScreenSaver.xml + * and documented in: + * https://projects.kde.org/projects/kde/kde-workspace/repository/revisions/master/entry/ksmserver/screenlocker/interface.h */ +static const gchar introspection_xml[] = +"<node name='/org/freedesktop/ScreenSaver'>" + "<interface name='org.freedesktop.ScreenSaver'>" + "<method name='Lock'/>" + "<method name='SimulateUserActivity'/>" + "<method name='GetActive'>" + "<arg type='b' direction='out'/>" + "</method>" + "<method name='GetActiveTime'>" + "<arg name='seconds' type='u' direction='out'/>" + "</method>" + "<method name='GetSessionIdleTime'>" + "<arg name='seconds' type='u' direction='out'/>" + "</method>" + "<method name='SetActive'>" + "<arg type='b' direction='out'/>" + "<arg name='e' type='b' direction='in'/>" + "</method>" + "<method name='Inhibit'>" + "<arg name='application_name' type='s' direction='in'/>" + "<arg name='reason_for_inhibit' type='s' direction='in'/>" + "<arg name='cookie' type='u' direction='out'/>" + "</method>" + "<method name='UnInhibit'>" + "<arg name='cookie' type='u' direction='in'/>" + "</method>" + "<method name='Throttle'>" + "<arg name='application_name' type='s' direction='in'/>" + "<arg name='reason_for_inhibit' type='s' direction='in'/>" + "<arg name='cookie' type='u' direction='out'/>" + "</method>" + "<method name='UnThrottle'>" + "<arg name='cookie' type='u' direction='in'/>" + "</method>" + + "<signal name='ActiveChanged'>" + "<arg type='b'/>" + "</signal>" + "</interface>" +"</node>"; +static const gchar introspection_xml2[] = +"<node name='/ScreenSaver'>" + "<interface name='org.freedesktop.ScreenSaver'>" + "<method name='Lock'/>" + "<method name='SimulateUserActivity'/>" + "<method name='GetActive'>" + "<arg type='b' direction='out'/>" + "</method>" + "<method name='GetActiveTime'>" + "<arg name='seconds' type='u' direction='out'/>" + "</method>" + "<method name='GetSessionIdleTime'>" + "<arg name='seconds' type='u' direction='out'/>" + "</method>" + "<method name='SetActive'>" + "<arg type='b' direction='out'/>" + "<arg name='e' type='b' direction='in'/>" + "</method>" + "<method name='Inhibit'>" + "<arg name='application_name' type='s' direction='in'/>" + "<arg name='reason_for_inhibit' type='s' direction='in'/>" + "<arg name='cookie' type='u' direction='out'/>" + "</method>" + "<method name='UnInhibit'>" + "<arg name='cookie' type='u' direction='in'/>" + "</method>" + "<method name='Throttle'>" + "<arg name='application_name' type='s' direction='in'/>" + "<arg name='reason_for_inhibit' type='s' direction='in'/>" + "<arg name='cookie' type='u' direction='out'/>" + "</method>" + "<method name='UnThrottle'>" + "<arg name='cookie' type='u' direction='in'/>" + "</method>" + + "<signal name='ActiveChanged'>" + "<arg type='b'/>" + "</signal>" + "</interface>" +"</node>"; + +#define GSD_SCREENSAVER_PROXY_DBUS_SERVICE "org.freedesktop.ScreenSaver" +#define GSD_SCREENSAVER_PROXY_DBUS_PATH "/org/freedesktop/ScreenSaver" +#define GSD_SCREENSAVER_PROXY_DBUS_PATH2 "/ScreenSaver" +#define GSD_SCREENSAVER_PROXY_DBUS_INTERFACE "org.freedesktop.ScreenSaver" + +#define GSM_INHIBITOR_FLAG_IDLE 1 << 3 + +struct _GsdScreensaverProxyManager +{ + GObject parent; + + GsdSessionManager *session; + GDBusConnection *connection; + GCancellable *bus_cancellable; + GDBusNodeInfo *introspection_data; + GDBusNodeInfo *introspection_data2; + guint name_id; + + GHashTable *watch_ht; /* key = sender, value = name watch id */ + GHashTable *cookie_ht; /* key = cookie, value = sender */ +}; + +static void gsd_screensaver_proxy_manager_class_init (GsdScreensaverProxyManagerClass *klass); +static void gsd_screensaver_proxy_manager_init (GsdScreensaverProxyManager *screensaver_proxy_manager); +static void gsd_screensaver_proxy_manager_finalize (GObject *object); + +G_DEFINE_TYPE (GsdScreensaverProxyManager, gsd_screensaver_proxy_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +static void +name_vanished_cb (GDBusConnection *connection, + const gchar *name, + GsdScreensaverProxyManager *manager) +{ + GHashTableIter iter; + gpointer cookie_ptr; + const char *sender; + + /* Look for all the cookies under that name, + * and call uninhibit for them */ + g_hash_table_iter_init (&iter, manager->cookie_ht); + while (g_hash_table_iter_next (&iter, &cookie_ptr, (gpointer *) &sender)) { + if (g_strcmp0 (sender, name) == 0) { + guint cookie = GPOINTER_TO_UINT (cookie_ptr); + + g_dbus_proxy_call_sync (G_DBUS_PROXY (manager->session), + "Uninhibit", + g_variant_new ("(u)", cookie), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL); + g_debug ("Removing cookie %u for sender %s", + cookie, sender); + g_hash_table_iter_remove (&iter); + } + } + + g_hash_table_remove (manager->watch_ht, name); +} + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GsdScreensaverProxyManager *manager = GSD_SCREENSAVER_PROXY_MANAGER (user_data); + g_autoptr(GError) error = NULL; + + /* Check session pointer as a proxy for whether the manager is in the + start or stop state */ + if (manager->session == NULL) { + g_dbus_method_invocation_return_dbus_error (invocation, + "org.freedesktop.DBus.Error.NotSupported", + "Session is unavailable"); + return; + } + + g_debug ("Calling method '%s.%s' for ScreenSaver Proxy", + interface_name, method_name); + + if (g_strcmp0 (method_name, "Inhibit") == 0) { + GVariant *ret; + const char *app_id; + const char *reason; + guint cookie; + + g_variant_get (parameters, + "(ss)", &app_id, &reason); + + ret = g_dbus_proxy_call_sync (G_DBUS_PROXY (G_DBUS_PROXY (manager->session)), + "Inhibit", + g_variant_new ("(susu)", + app_id, 0, reason, GSM_INHIBITOR_FLAG_IDLE), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, &error); + + if (!ret) + goto error; + + g_variant_get (ret, "(u)", &cookie); + g_hash_table_insert (manager->cookie_ht, + GUINT_TO_POINTER (cookie), + g_strdup (sender)); + if (g_hash_table_lookup (manager->watch_ht, sender) == NULL) { + guint watch_id; + + watch_id = g_bus_watch_name_on_connection (manager->connection, + sender, + G_BUS_NAME_WATCHER_FLAGS_NONE, + NULL, + (GBusNameVanishedCallback) name_vanished_cb, + manager, + NULL); + g_hash_table_insert (manager->watch_ht, + g_strdup (sender), + GUINT_TO_POINTER (watch_id)); + } + g_dbus_method_invocation_return_value (invocation, ret); + } else if (g_strcmp0 (method_name, "UnInhibit") == 0) { + GVariant *ret; + guint cookie; + + g_variant_get (parameters, "(u)", &cookie); + ret = g_dbus_proxy_call_sync (G_DBUS_PROXY (manager->session), + "Uninhibit", + parameters, + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, &error); + + if (!ret) + goto error; + + g_debug ("Removing cookie %u from the list for %s", cookie, sender); + g_hash_table_remove (manager->cookie_ht, GUINT_TO_POINTER (cookie)); + g_dbus_method_invocation_return_value (invocation, NULL); + } else if (g_strcmp0 (method_name, "Throttle") == 0) { + g_dbus_method_invocation_return_value (invocation, NULL); + } else if (g_strcmp0 (method_name, "UnThrottle") == 0) { + g_dbus_method_invocation_return_value (invocation, NULL); + } else if (g_strcmp0 (method_name, "Lock") == 0) { + goto unimplemented; + } else if (g_strcmp0 (method_name, "SimulateUserActivity") == 0) { + goto unimplemented; + } else if (g_strcmp0 (method_name, "GetActive") == 0) { + goto unimplemented; + } else if (g_strcmp0 (method_name, "GetActiveTime") == 0) { + goto unimplemented; + } else if (g_strcmp0 (method_name, "GetSessionIdleTime") == 0) { + goto unimplemented; + } else if (g_strcmp0 (method_name, "SetActive") == 0) { + goto unimplemented; + } + + return; + +unimplemented: + g_dbus_method_invocation_return_dbus_error (invocation, + "org.freedesktop.DBus.Error.NotSupported", + "This method is not implemented"); + return; +error: + g_dbus_method_invocation_return_gerror (invocation, error); +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + NULL, /* GetProperty */ + NULL, /* SetProperty */ +}; + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *res, + GsdScreensaverProxyManager *manager) +{ + GDBusConnection *connection; + GDBusInterfaceInfo **infos; + GError *error = NULL; + + if (manager->bus_cancellable == NULL || + g_cancellable_is_cancelled (manager->bus_cancellable)) { + g_warning ("Operation has been cancelled, so not retrieving session bus"); + return; + } + + connection = g_bus_get_finish (res, &error); + if (connection == NULL) { + g_warning ("Could not get session bus: %s", error->message); + g_error_free (error); + return; + } + manager->connection = connection; + infos = manager->introspection_data->interfaces; + g_dbus_connection_register_object (connection, + GSD_SCREENSAVER_PROXY_DBUS_PATH, + infos[0], + &interface_vtable, + manager, + NULL, + NULL); + infos = manager->introspection_data2->interfaces; + g_dbus_connection_register_object (connection, + GSD_SCREENSAVER_PROXY_DBUS_PATH2, + infos[0], + &interface_vtable, + manager, + NULL, + NULL); + + manager->name_id = g_bus_own_name_on_connection (manager->connection, + GSD_SCREENSAVER_PROXY_DBUS_SERVICE, + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, + NULL, + NULL, + NULL); +} + +static void +register_manager_dbus (GsdScreensaverProxyManager *manager) +{ + manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + manager->introspection_data2 = g_dbus_node_info_new_for_xml (introspection_xml2, NULL); + manager->bus_cancellable = g_cancellable_new (); + g_assert (manager->introspection_data != NULL); + g_assert (manager->introspection_data2 != NULL); + + g_bus_get (G_BUS_TYPE_SESSION, + manager->bus_cancellable, + (GAsyncReadyCallback) on_bus_gotten, + manager); +} + +gboolean +gsd_screensaver_proxy_manager_start (GsdScreensaverProxyManager *manager, + GError **error) +{ + g_debug ("Starting screensaver-proxy manager"); + gnome_settings_profile_start (NULL); + manager->session = + gnome_settings_bus_get_session_proxy (); + manager->watch_ht = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_bus_unwatch_name); + manager->cookie_ht = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + NULL, + (GDestroyNotify) g_free); + gnome_settings_profile_end (NULL); + return TRUE; +} + +void +gsd_screensaver_proxy_manager_stop (GsdScreensaverProxyManager *manager) +{ + g_debug ("Stopping screensaver_proxy manager"); + g_clear_object (&manager->session); + g_clear_pointer (&manager->watch_ht, g_hash_table_destroy); + g_clear_pointer (&manager->cookie_ht, g_hash_table_destroy); +} + +static void +gsd_screensaver_proxy_manager_class_init (GsdScreensaverProxyManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_screensaver_proxy_manager_finalize; +} + +static void +gsd_screensaver_proxy_manager_init (GsdScreensaverProxyManager *manager) +{ +} + +static void +gsd_screensaver_proxy_manager_finalize (GObject *object) +{ + GsdScreensaverProxyManager *manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_SCREENSAVER_PROXY_MANAGER (object)); + + manager = GSD_SCREENSAVER_PROXY_MANAGER (object); + + g_return_if_fail (manager != NULL); + + gsd_screensaver_proxy_manager_stop (manager); + + if (manager->name_id != 0) { + g_bus_unown_name (manager->name_id); + manager->name_id = 0; + } + g_clear_object (&manager->connection); + g_clear_object (&manager->bus_cancellable); + g_clear_pointer (&manager->introspection_data, g_dbus_node_info_unref); + g_clear_pointer (&manager->introspection_data2, g_dbus_node_info_unref); + + G_OBJECT_CLASS (gsd_screensaver_proxy_manager_parent_class)->finalize (object); +} + +GsdScreensaverProxyManager * +gsd_screensaver_proxy_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_SCREENSAVER_PROXY_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + register_manager_dbus (manager_object); + } + + return GSD_SCREENSAVER_PROXY_MANAGER (manager_object); +} diff --git a/plugins/screensaver-proxy/gsd-screensaver-proxy-manager.h b/plugins/screensaver-proxy/gsd-screensaver-proxy-manager.h new file mode 100644 index 0000000..c2f15ad --- /dev/null +++ b/plugins/screensaver-proxy/gsd-screensaver-proxy-manager.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2012 Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_SCREENSAVER_PROXY_MANAGER_H +#define __GSD_SCREENSAVER_PROXY_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_SCREENSAVER_PROXY_MANAGER (gsd_screensaver_proxy_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdScreensaverProxyManager, gsd_screensaver_proxy_manager, GSD, SCREENSAVER_PROXY_MANAGER, GObject) + +GsdScreensaverProxyManager *gsd_screensaver_proxy_manager_new (void); +gboolean gsd_screensaver_proxy_manager_start (GsdScreensaverProxyManager *manager, + GError **error); +void gsd_screensaver_proxy_manager_stop (GsdScreensaverProxyManager *manager); + +G_END_DECLS + +#endif /* __GSD_SCREENSAVER_PROXY_MANAGER_H */ diff --git a/plugins/screensaver-proxy/main.c b/plugins/screensaver-proxy/main.c new file mode 100644 index 0000000..3e3af27 --- /dev/null +++ b/plugins/screensaver-proxy/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_screensaver_proxy_manager_new +#define START gsd_screensaver_proxy_manager_start +#define STOP gsd_screensaver_proxy_manager_stop +#define MANAGER GsdScreensaverProxyManager +#include "gsd-screensaver-proxy-manager.h" + +#include "daemon-skeleton.h" diff --git a/plugins/screensaver-proxy/meson.build b/plugins/screensaver-proxy/meson.build new file mode 100644 index 0000000..5430eb8 --- /dev/null +++ b/plugins/screensaver-proxy/meson.build @@ -0,0 +1,17 @@ +sources = files( + 'gsd-screensaver-proxy-manager.c', + 'main.c' +) + +deps = plugins_deps + [gio_dep] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) diff --git a/plugins/sharing/gsd-sharing-enums.h b/plugins/sharing/gsd-sharing-enums.h new file mode 100644 index 0000000..e9b2fc7 --- /dev/null +++ b/plugins/sharing/gsd-sharing-enums.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2014 Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_SHARING_ENUMS_H +#define __GSD_SHARING_ENUMS_H + +G_BEGIN_DECLS + +typedef enum { + GSD_SHARING_STATUS_OFFLINE, + GSD_SHARING_STATUS_DISABLED_MOBILE_BROADBAND, + GSD_SHARING_STATUS_DISABLED_LOW_SECURITY, + GSD_SHARING_STATUS_AVAILABLE +} GsdSharingStatus; + +G_END_DECLS + +#endif /* __GSD_SHARING_ENUMS_H */ diff --git a/plugins/sharing/gsd-sharing-manager.c b/plugins/sharing/gsd-sharing-manager.c new file mode 100644 index 0000000..43067cc --- /dev/null +++ b/plugins/sharing/gsd-sharing-manager.c @@ -0,0 +1,829 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2014 Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <locale.h> +#include <glib.h> +#include <gio/gio.h> +#include <gio/gdesktopappinfo.h> +#include <glib/gstdio.h> + +#if HAVE_NETWORK_MANAGER +#include <NetworkManager.h> +#endif /* HAVE_NETWORK_MANAGER */ + +#include "gnome-settings-profile.h" +#include "gsd-sharing-manager.h" +#include "gsd-sharing-enums.h" + +typedef struct { + const char *name; + GSettings *settings; +} ServiceInfo; + +struct _GsdSharingManager +{ + GObject parent; + + GDBusNodeInfo *introspection_data; + guint name_id; + GDBusConnection *connection; + + GCancellable *cancellable; +#if HAVE_NETWORK_MANAGER + NMClient *client; +#endif /* HAVE_NETWORK_MANAGER */ + + GHashTable *services; + + char *current_network; + char *current_network_name; + char *carrier_type; + GsdSharingStatus sharing_status; +}; + +#define GSD_DBUS_NAME "org.gnome.SettingsDaemon" +#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon" +#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon" + +#define GSD_SHARING_DBUS_NAME GSD_DBUS_NAME ".Sharing" +#define GSD_SHARING_DBUS_PATH GSD_DBUS_PATH "/Sharing" + +static const gchar introspection_xml[] = +"<node>" +" <interface name='org.gnome.SettingsDaemon.Sharing'>" +" <annotation name='org.freedesktop.DBus.GLib.CSymbol' value='gsd_sharing_manager'/>" +" <property name='CurrentNetwork' type='s' access='read'/>" +" <property name='CurrentNetworkName' type='s' access='read'/>" +" <property name='CarrierType' type='s' access='read'/>" +" <property name='SharingStatus' type='u' access='read'/>" +" <method name='EnableService'>" +" <arg name='service-name' direction='in' type='s'/>" +" </method>" +" <method name='DisableService'>" +" <arg name='service-name' direction='in' type='s'/>" +" <arg name='network' direction='in' type='s'/>" +" </method>" +" <method name='ListNetworks'>" +" <arg name='service-name' direction='in' type='s'/>" +" <arg name='networks' direction='out' type='a(sss)'/>" +" </method>" +" </interface>" +"</node>"; + +static void gsd_sharing_manager_class_init (GsdSharingManagerClass *klass); +static void gsd_sharing_manager_init (GsdSharingManager *manager); +static void gsd_sharing_manager_finalize (GObject *object); + +G_DEFINE_TYPE (GsdSharingManager, gsd_sharing_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +static const char * const services[] = { + "rygel", + "gnome-remote-desktop", + "gnome-user-share-webdav" +}; + +static void +handle_unit_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GError *error = NULL; + GVariant *ret; + const char *operation = user_data; + + ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), + res, &error); + if (!ret) { + g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error); + + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + g_strcmp0 (remote_error, "org.freedesktop.systemd1.NoSuchUnit") != 0) + g_warning ("Failed to %s service: %s", operation, error->message); + g_error_free (error); + return; + } + + g_variant_unref (ret); + +} + +static void +gsd_sharing_manager_handle_service (GsdSharingManager *manager, + const char *method, + ServiceInfo *service) +{ + char *service_file; + + service_file = g_strdup_printf ("%s.service", service->name); + g_dbus_connection_call (manager->connection, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + method, + g_variant_new ("(ss)", service_file, "replace"), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + handle_unit_cb, + (gpointer) method); + g_free (service_file); +} + +static void +gsd_sharing_manager_start_service (GsdSharingManager *manager, + ServiceInfo *service) +{ + g_debug ("About to start %s", service->name); + + /* We use StartUnit, not StartUnitReplace, since the latter would + * cancel any pending start we already have going from an + * earlier _start_service() call */ + gsd_sharing_manager_handle_service (manager, "StartUnit", service); +} + +static void +gsd_sharing_manager_stop_service (GsdSharingManager *manager, + ServiceInfo *service) +{ + g_debug ("About to stop %s", service->name); + + gsd_sharing_manager_handle_service (manager, "StopUnit", service); +} + +#if HAVE_NETWORK_MANAGER +static gboolean +service_is_enabled_on_current_connection (GsdSharingManager *manager, + ServiceInfo *service) +{ + char **connections; + int j; + gboolean ret; + connections = g_settings_get_strv (service->settings, "enabled-connections"); + ret = FALSE; + for (j = 0; connections[j] != NULL; j++) { + if (g_strcmp0 (connections[j], manager->current_network) == 0) { + ret = TRUE; + break; + } + } + + g_strfreev (connections); + return ret; +} +#else +static gboolean +service_is_enabled_on_current_connection (GsdSharingManager *manager, + ServiceInfo *service) +{ + return FALSE; +} +#endif /* HAVE_NETWORK_MANAGER */ + +static void +gsd_sharing_manager_sync_services (GsdSharingManager *manager) +{ + GList *services, *l; + + services = g_hash_table_get_values (manager->services); + + for (l = services; l != NULL; l = l->next) { + ServiceInfo *service = l->data; + gboolean should_be_started = FALSE; + + if (manager->sharing_status == GSD_SHARING_STATUS_AVAILABLE && + service_is_enabled_on_current_connection (manager, service)) + should_be_started = TRUE; + + if (should_be_started) + gsd_sharing_manager_start_service (manager, service); + else + gsd_sharing_manager_stop_service (manager, service); + } + g_list_free (services); +} + +#if HAVE_NETWORK_MANAGER +static void +properties_changed (GsdSharingManager *manager) +{ + GVariantBuilder props_builder; + GVariant *props_changed = NULL; + + /* not yet connected to the session bus */ + if (manager->connection == NULL) + return; + + g_variant_builder_init (&props_builder, G_VARIANT_TYPE ("a{sv}")); + + g_variant_builder_add (&props_builder, "{sv}", "CurrentNetwork", + g_variant_new_string (manager->current_network)); + g_variant_builder_add (&props_builder, "{sv}", "CurrentNetworkName", + g_variant_new_string (manager->current_network_name)); + g_variant_builder_add (&props_builder, "{sv}", "CarrierType", + g_variant_new_string (manager->carrier_type)); + g_variant_builder_add (&props_builder, "{sv}", "SharingStatus", + g_variant_new_uint32 (manager->sharing_status)); + + props_changed = g_variant_new ("(s@a{sv}@as)", GSD_SHARING_DBUS_NAME, + g_variant_builder_end (&props_builder), + g_variant_new_strv (NULL, 0)); + + g_dbus_connection_emit_signal (manager->connection, + NULL, + GSD_SHARING_DBUS_PATH, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + props_changed, NULL); +} + +static char ** +get_connections_for_service (GsdSharingManager *manager, + const char *service_name) +{ + ServiceInfo *service; + + service = g_hash_table_lookup (manager->services, service_name); + return g_settings_get_strv (service->settings, "enabled-connections"); +} +#else +static char ** +get_connections_for_service (GsdSharingManager *manager, + const char *service_name) +{ + const char * const * connections [] = { NULL }; + return g_strdupv ((char **) connections); +} +#endif /* HAVE_NETWORK_MANAGER */ + +static gboolean +check_service (GsdSharingManager *manager, + const char *service_name, + GError **error) +{ + if (g_hash_table_lookup (manager->services, service_name)) + return TRUE; + + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, + "Invalid service name '%s'", service_name); + return FALSE; +} + +static gboolean +gsd_sharing_manager_enable_service (GsdSharingManager *manager, + const char *service_name, + GError **error) +{ + ServiceInfo *service; + char **connections; + GPtrArray *array; + guint i; + + if (!check_service (manager, service_name, error)) + return FALSE; + + if (manager->sharing_status != GSD_SHARING_STATUS_AVAILABLE) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, + "Sharing cannot be enabled on this network, status is '%d'", manager->sharing_status); + return FALSE; + } + + service = g_hash_table_lookup (manager->services, service_name); + connections = g_settings_get_strv (service->settings, "enabled-connections"); + array = g_ptr_array_new (); + for (i = 0; connections[i] != NULL; i++) { + if (g_strcmp0 (connections[i], manager->current_network) == 0) + goto bail; + g_ptr_array_add (array, connections[i]); + } + g_ptr_array_add (array, manager->current_network); + g_ptr_array_add (array, NULL); + + g_settings_set_strv (service->settings, "enabled-connections", (const gchar *const *) array->pdata); + +bail: + + gsd_sharing_manager_start_service (manager, service); + + g_ptr_array_unref (array); + g_strfreev (connections); + + return TRUE; +} + +static gboolean +gsd_sharing_manager_disable_service (GsdSharingManager *manager, + const char *service_name, + const char *network_name, + GError **error) +{ + ServiceInfo *service; + char **connections; + GPtrArray *array; + guint i; + + if (!check_service (manager, service_name, error)) + return FALSE; + + service = g_hash_table_lookup (manager->services, service_name); + connections = g_settings_get_strv (service->settings, "enabled-connections"); + array = g_ptr_array_new (); + for (i = 0; connections[i] != NULL; i++) { + if (g_strcmp0 (connections[i], network_name) != 0) + g_ptr_array_add (array, connections[i]); + } + g_ptr_array_add (array, NULL); + + g_settings_set_strv (service->settings, "enabled-connections", (const gchar *const *) array->pdata); + g_ptr_array_unref (array); + g_strfreev (connections); + + if (g_str_equal (network_name, manager->current_network)) + gsd_sharing_manager_stop_service (manager, service); + + return TRUE; +} + +#if HAVE_NETWORK_MANAGER +static const char * +get_type_and_name_for_connection_uuid (GsdSharingManager *manager, + const char *uuid, + const char **name) +{ + NMRemoteConnection *conn; + const char *type; + + if (!manager->client) + return NULL; + + conn = nm_client_get_connection_by_uuid (manager->client, uuid); + if (!conn) + return NULL; + type = nm_connection_get_connection_type (NM_CONNECTION (conn)); + *name = nm_connection_get_id (NM_CONNECTION (conn)); + + return type; +} +#else +static const char * +get_type_and_name_for_connection_uuid (GsdSharingManager *manager, + const char *id, + const char **name) +{ + return NULL; +} +#endif /* HAVE_NETWORK_MANAGER */ + +#if HAVE_NETWORK_MANAGER +static gboolean +connection_is_low_security (GsdSharingManager *manager, + const char *uuid) +{ + NMRemoteConnection *conn; + + if (!manager->client) + return TRUE; + + conn = nm_client_get_connection_by_uuid (manager->client, uuid); + if (!conn) + return TRUE; + + /* Disable sharing on open Wi-Fi + * XXX: Also do this for WEP networks? */ + return (nm_connection_get_setting_wireless_security (NM_CONNECTION (conn)) == NULL); +} +#endif /* HAVE_NETWORK_MANAGER */ + +static GVariant * +gsd_sharing_manager_list_networks (GsdSharingManager *manager, + const char *service_name, + GError **error) +{ + char **connections; + GVariantBuilder builder; + guint i; + + if (!check_service (manager, service_name, error)) + return NULL; + +#if HAVE_NETWORK_MANAGER + if (!manager->client) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Not ready yet"); + return NULL; + } +#endif /* HAVE_NETWORK_MANAGER */ + + connections = get_connections_for_service (manager, service_name); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("(a(sss))")); + g_variant_builder_open (&builder, G_VARIANT_TYPE ("a(sss)")); + + for (i = 0; connections[i] != NULL; i++) { + const char *type, *name; + + type = get_type_and_name_for_connection_uuid (manager, connections[i], &name); + if (!type) + continue; + + g_variant_builder_add (&builder, "(sss)", connections[i], name, type); + } + g_strfreev (connections); + + g_variant_builder_close (&builder); + + return g_variant_builder_end (&builder); +} + +static GVariant * +handle_get_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, + gpointer user_data) +{ + GsdSharingManager *manager = GSD_SHARING_MANAGER (user_data); + + /* Check session pointer as a proxy for whether the manager is in the + start or stop state */ + if (manager->connection == NULL) + return NULL; + + if (g_strcmp0 (property_name, "CurrentNetwork") == 0) { + return g_variant_new_string (manager->current_network); + } + + if (g_strcmp0 (property_name, "CurrentNetworkName") == 0) { + return g_variant_new_string (manager->current_network_name); + } + + if (g_strcmp0 (property_name, "CarrierType") == 0) { + return g_variant_new_string (manager->carrier_type); + } + + if (g_strcmp0 (property_name, "SharingStatus") == 0) { + return g_variant_new_uint32 (manager->sharing_status); + } + + return NULL; +} + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GsdSharingManager *manager = (GsdSharingManager *) user_data; + + g_debug ("Calling method '%s' for sharing", method_name); + + /* Check session pointer as a proxy for whether the manager is in the + start or stop state */ + if (manager->connection == NULL) + return; + + if (g_strcmp0 (method_name, "EnableService") == 0) { + const char *service; + GError *error = NULL; + + g_variant_get (parameters, "(&s)", &service); + if (!gsd_sharing_manager_enable_service (manager, service, &error)) + g_dbus_method_invocation_take_error (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, NULL); + } else if (g_strcmp0 (method_name, "DisableService") == 0) { + const char *service; + const char *network_name; + GError *error = NULL; + + g_variant_get (parameters, "(&s&s)", &service, &network_name); + if (!gsd_sharing_manager_disable_service (manager, service, network_name, &error)) + g_dbus_method_invocation_take_error (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, NULL); + } else if (g_strcmp0 (method_name, "ListNetworks") == 0) { + const char *service; + GError *error = NULL; + GVariant *variant; + + g_variant_get (parameters, "(&s)", &service); + variant = gsd_sharing_manager_list_networks (manager, service, &error); + if (!variant) + g_dbus_method_invocation_take_error (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, variant); + } +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + handle_get_property, + NULL +}; + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *res, + GsdSharingManager *manager) +{ + GDBusConnection *connection; + GError *error = NULL; + + connection = g_bus_get_finish (res, &error); + if (connection == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Could not get session bus: %s", error->message); + g_error_free (error); + return; + } + manager->connection = connection; + + g_dbus_connection_register_object (connection, + GSD_SHARING_DBUS_PATH, + manager->introspection_data->interfaces[0], + &interface_vtable, + manager, + NULL, + NULL); + + manager->name_id = g_bus_own_name_on_connection (connection, + GSD_SHARING_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, + NULL, + NULL, + NULL); +} + +#if HAVE_NETWORK_MANAGER +static void +primary_connection_changed (GObject *gobject, + GParamSpec *pspec, + gpointer user_data) +{ + GsdSharingManager *manager = user_data; + NMActiveConnection *a_con; + + a_con = nm_client_get_primary_connection (manager->client); + + g_clear_pointer (&manager->current_network, g_free); + g_clear_pointer (&manager->current_network_name, g_free); + g_clear_pointer (&manager->carrier_type, g_free); + + if (a_con) { + manager->current_network = g_strdup (nm_active_connection_get_uuid (a_con)); + manager->current_network_name = g_strdup (nm_active_connection_get_id (a_con)); + manager->carrier_type = g_strdup (nm_active_connection_get_connection_type (a_con)); + if (manager->carrier_type == NULL) + manager->carrier_type = g_strdup (""); + } else { + manager->current_network = g_strdup (""); + manager->current_network_name = g_strdup (""); + manager->carrier_type = g_strdup (""); + } + + if (!a_con) { + manager->sharing_status = GSD_SHARING_STATUS_OFFLINE; + } else if (*(manager->carrier_type) == '\0') { + /* Missing carrier type information? */ + manager->sharing_status = GSD_SHARING_STATUS_OFFLINE; + } else if (g_str_equal (manager->carrier_type, "bluetooth") || + g_str_equal (manager->carrier_type, "gsm") || + g_str_equal (manager->carrier_type, "cdma")) { + manager->sharing_status = GSD_SHARING_STATUS_DISABLED_MOBILE_BROADBAND; + } else if (g_str_equal (manager->carrier_type, "802-11-wireless")) { + if (connection_is_low_security (manager, manager->current_network)) + manager->sharing_status = GSD_SHARING_STATUS_DISABLED_LOW_SECURITY; + else + manager->sharing_status = GSD_SHARING_STATUS_AVAILABLE; + } else { + manager->sharing_status = GSD_SHARING_STATUS_AVAILABLE; + } + + g_debug ("current network: %s", manager->current_network); + g_debug ("current network name: %s", manager->current_network_name); + g_debug ("conn type: %s", manager->carrier_type); + g_debug ("status: %d", manager->sharing_status); + + properties_changed (manager); + gsd_sharing_manager_sync_services (manager); +} + +static void +nm_client_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsdSharingManager *manager = user_data; + GError *error = NULL; + NMClient *client; + + client = nm_client_new_finish (res, &error); + if (!client) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Couldn't get NMClient: %s", error->message); + g_error_free (error); + return; + } + manager->client = client; + + g_signal_connect (G_OBJECT (client), "notify::primary-connection", + G_CALLBACK (primary_connection_changed), manager); + + primary_connection_changed (NULL, NULL, manager); +} + +#endif /* HAVE_NETWORK_MANAGER */ + +#define RYGEL_BUS_NAME "org.gnome.Rygel1" +#define RYGEL_OBJECT_PATH "/org/gnome/Rygel1" +#define RYGEL_INTERFACE_NAME "org.gnome.Rygel1" + +static void +gsd_sharing_manager_disable_rygel (void) +{ + GDBusConnection *connection; + gchar *path; + + path = g_build_filename (g_get_user_config_dir (), "autostart", + "rygel.desktop", NULL); + if (!g_file_test (path, G_FILE_TEST_IS_SYMLINK | G_FILE_TEST_IS_REGULAR)) + goto out; + + g_unlink (path); + + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + if (connection) { + g_dbus_connection_call (connection, RYGEL_BUS_NAME, RYGEL_OBJECT_PATH, RYGEL_INTERFACE_NAME, + "Shutdown", NULL, NULL, G_DBUS_CALL_FLAGS_NONE, -1, + NULL, NULL, NULL); + } + g_object_unref (connection); + + out: + g_free (path); +} + +gboolean +gsd_sharing_manager_start (GsdSharingManager *manager, + GError **error) +{ + g_debug ("Starting sharing manager"); + gnome_settings_profile_start (NULL); + + manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + g_assert (manager->introspection_data != NULL); + + gsd_sharing_manager_disable_rygel (); + + manager->cancellable = g_cancellable_new (); +#if HAVE_NETWORK_MANAGER + nm_client_new_async (manager->cancellable, nm_client_ready, manager); +#endif /* HAVE_NETWORK_MANAGER */ + + /* Start process of owning a D-Bus name */ + g_bus_get (G_BUS_TYPE_SESSION, + manager->cancellable, + (GAsyncReadyCallback) on_bus_gotten, + manager); + + gnome_settings_profile_end (NULL); + return TRUE; +} + +void +gsd_sharing_manager_stop (GsdSharingManager *manager) +{ + g_debug ("Stopping sharing manager"); + + if (manager->sharing_status == GSD_SHARING_STATUS_AVAILABLE && + manager->connection != NULL) { + manager->sharing_status = GSD_SHARING_STATUS_OFFLINE; + gsd_sharing_manager_sync_services (manager); + } + + if (manager->cancellable) { + g_cancellable_cancel (manager->cancellable); + g_clear_object (&manager->cancellable); + } + +#if HAVE_NETWORK_MANAGER + g_clear_object (&manager->client); +#endif /* HAVE_NETWORK_MANAGER */ + + if (manager->name_id != 0) { + g_bus_unown_name (manager->name_id); + manager->name_id = 0; + } + + g_clear_pointer (&manager->introspection_data, g_dbus_node_info_unref); + g_clear_object (&manager->connection); + + g_clear_pointer (&manager->current_network, g_free); + g_clear_pointer (&manager->current_network_name, g_free); + g_clear_pointer (&manager->carrier_type, g_free); +} + +static void +gsd_sharing_manager_class_init (GsdSharingManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_sharing_manager_finalize; +} + +static void +service_free (gpointer pointer) +{ + ServiceInfo *service = pointer; + + g_clear_object (&service->settings); + g_free (service); +} + +static void +gsd_sharing_manager_init (GsdSharingManager *manager) +{ + guint i; + + manager->services = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, service_free); + + /* Default state */ + manager->current_network = g_strdup (""); + manager->current_network_name = g_strdup (""); + manager->carrier_type = g_strdup (""); + manager->sharing_status = GSD_SHARING_STATUS_OFFLINE; + + for (i = 0; i < G_N_ELEMENTS (services); i++) { + ServiceInfo *service; + char *path; + + service = g_new0 (ServiceInfo, 1); + service->name = services[i]; + path = g_strdup_printf ("/org/gnome/settings-daemon/plugins/sharing/%s/", services[i]); + service->settings = g_settings_new_with_path ("org.gnome.settings-daemon.plugins.sharing.service", path); + g_free (path); + + g_hash_table_insert (manager->services, (gpointer) services[i], service); + } +} + +static void +gsd_sharing_manager_finalize (GObject *object) +{ + GsdSharingManager *manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_SHARING_MANAGER (object)); + + manager = GSD_SHARING_MANAGER (object); + + g_return_if_fail (manager != NULL); + + gsd_sharing_manager_stop (manager); + + g_hash_table_unref (manager->services); + + G_OBJECT_CLASS (gsd_sharing_manager_parent_class)->finalize (object); +} + +GsdSharingManager * +gsd_sharing_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_SHARING_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_SHARING_MANAGER (manager_object); +} diff --git a/plugins/sharing/gsd-sharing-manager.h b/plugins/sharing/gsd-sharing-manager.h new file mode 100644 index 0000000..0087abc --- /dev/null +++ b/plugins/sharing/gsd-sharing-manager.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_SHARING_MANAGER_H +#define __GSD_SHARING_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_SHARING_MANAGER (gsd_sharing_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdSharingManager, gsd_sharing_manager, GSD, SHARING_MANAGER, GObject) + +GsdSharingManager * gsd_sharing_manager_new (void); +gboolean gsd_sharing_manager_start (GsdSharingManager *manager, + GError **error); +void gsd_sharing_manager_stop (GsdSharingManager *manager); + +G_END_DECLS + +#endif /* __GSD_SHARING_MANAGER_H */ diff --git a/plugins/sharing/main.c b/plugins/sharing/main.c new file mode 100644 index 0000000..656e27d --- /dev/null +++ b/plugins/sharing/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_sharing_manager_new +#define START gsd_sharing_manager_start +#define STOP gsd_sharing_manager_stop +#define MANAGER GsdSharingManager +#include "gsd-sharing-manager.h" + +#include "daemon-skeleton.h" diff --git a/plugins/sharing/meson.build b/plugins/sharing/meson.build new file mode 100644 index 0000000..9484312 --- /dev/null +++ b/plugins/sharing/meson.build @@ -0,0 +1,24 @@ +sources = files( + 'gsd-sharing-manager.c', + 'main.c' +) + +deps = plugins_deps + [ + gio_unix_dep, + libnotify_dep +] + +if enable_network_manager + deps += libnm_dep +endif + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) diff --git a/plugins/smartcard/gsd-smartcard-enum-types.c.in b/plugins/smartcard/gsd-smartcard-enum-types.c.in new file mode 100644 index 0000000..f281cf4 --- /dev/null +++ b/plugins/smartcard/gsd-smartcard-enum-types.c.in @@ -0,0 +1,42 @@ +/*** BEGIN file-header ***/ + +#include <glib-object.h> + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +#include "@filename@" +/* enumerations from "@filename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType @enum_name@_get_type (void) G_GNUC_CONST; + +GType +@enum_name@_get_type (void) +{ + static GType etype = 0; + + if (G_UNLIKELY(etype == 0)) { + static const G@Type@Value values[] = { +/*** END value-header ***/ + +/*** BEGIN value-production ***/ + { @VALUENAME@, "@VALUENAME@", "@valuenick@" }, +/*** END value-production ***/ + +/*** BEGIN value-tail ***/ + { 0, NULL, NULL } + }; + + etype = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values); + } + + return etype; +} + +/*** END value-tail ***/ + +/*** BEGIN file-tail ***/ + /**/ +/*** END file-tail ***/ diff --git a/plugins/smartcard/gsd-smartcard-enum-types.h.in b/plugins/smartcard/gsd-smartcard-enum-types.h.in new file mode 100644 index 0000000..79dcc3d --- /dev/null +++ b/plugins/smartcard/gsd-smartcard-enum-types.h.in @@ -0,0 +1,24 @@ +/*** BEGIN file-header ***/ +#ifndef GSD_IDENTITY_ENUM_TYPES_H +#define GSD_IDENTITY_ENUM_TYPES_H + +#include <glib-object.h> + +G_BEGIN_DECLS +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* enumerations from "@filename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType @enum_name@_get_type (void) G_GNUC_CONST; +#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) +/*** END value-header ***/ + +/*** BEGIN file-tail ***/ +G_END_DECLS + +#endif /* GSD_IDENTITY_ENUM_TYPES_H */ +/*** END file-tail ***/ diff --git a/plugins/smartcard/gsd-smartcard-manager.c b/plugins/smartcard/gsd-smartcard-manager.c new file mode 100644 index 0000000..fc6ae0b --- /dev/null +++ b/plugins/smartcard/gsd-smartcard-manager.c @@ -0,0 +1,993 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2010,2011 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <glib.h> +#include <gio/gio.h> + +#include "gnome-settings-profile.h" +#include "gnome-settings-bus.h" +#include "gsd-smartcard-manager.h" +#include "gsd-smartcard-service.h" +#include "gsd-smartcard-enum-types.h" +#include "gsd-smartcard-utils.h" + +#include <prerror.h> +#include <prinit.h> +#include <nss.h> +#include <pk11func.h> +#include <secmod.h> +#include <secerr.h> + +#define GSD_SESSION_MANAGER_LOGOUT_MODE_FORCE 2 + +struct _GsdSmartcardManager +{ + GObject parent; + + guint start_idle_id; + GsdSmartcardService *service; + GList *smartcards_watch_tasks; + GCancellable *cancellable; + + GsdSessionManager *session_manager; + GsdScreenSaver *screen_saver; + + GSettings *settings; + + NSSInitContext *nss_context; +}; + +#define CONF_SCHEMA "org.gnome.settings-daemon.peripherals.smartcard" +#define KEY_REMOVE_ACTION "removal-action" + +static void gsd_smartcard_manager_class_init (GsdSmartcardManagerClass *klass); +static void gsd_smartcard_manager_init (GsdSmartcardManager *self); +static void gsd_smartcard_manager_finalize (GObject *object); +static void lock_screen (GsdSmartcardManager *self); +static void log_out (GsdSmartcardManager *self); +static void on_smartcards_from_driver_watched (GsdSmartcardManager *self, + GAsyncResult *result, + GTask *task); +G_DEFINE_TYPE (GsdSmartcardManager, gsd_smartcard_manager, G_TYPE_OBJECT) +G_DEFINE_QUARK (gsd-smartcard-manager-error, gsd_smartcard_manager_error) +G_LOCK_DEFINE_STATIC (gsd_smartcards_watch_tasks); + +typedef struct { + SECMODModule *driver; + guint idle_id; + GError *error; +} DriverRegistrationOperation; + +static gpointer manager_object = NULL; + +static void +gsd_smartcard_manager_class_init (GsdSmartcardManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_smartcard_manager_finalize; + + gsd_smartcard_utils_register_error_domain (GSD_SMARTCARD_MANAGER_ERROR, + GSD_TYPE_SMARTCARD_MANAGER_ERROR); +} + +static void +gsd_smartcard_manager_init (GsdSmartcardManager *self) +{ +} + +static void +load_nss (GsdSmartcardManager *self) +{ + NSSInitContext *context = NULL; + + /* The first field in the NSSInitParameters structure + * is the size of the structure. NSS requires this, so + * that it can change the size of the structure in future + * versions of NSS in a detectable way + */ + NSSInitParameters parameters = { sizeof (parameters), }; + static const guint32 flags = NSS_INIT_READONLY + | NSS_INIT_FORCEOPEN + | NSS_INIT_NOROOTINIT + | NSS_INIT_OPTIMIZESPACE + | NSS_INIT_PK11RELOAD; + + g_debug ("attempting to load NSS database '%s'", + GSD_SMARTCARD_MANAGER_NSS_DB); + + PR_Init (PR_USER_THREAD, PR_PRIORITY_NORMAL, 0); + + context = NSS_InitContext (GSD_SMARTCARD_MANAGER_NSS_DB, + "", "", SECMOD_DB, ¶meters, flags); + + if (context == NULL) { + gsize error_message_size; + char *error_message; + + error_message_size = PR_GetErrorTextLength (); + + if (error_message_size == 0) { + g_debug ("NSS security system could not be initialized"); + } else { + error_message = g_alloca (error_message_size); + PR_GetErrorText (error_message); + + g_debug ("NSS security system could not be initialized - %s", + error_message); + } + + self->nss_context = NULL; + return; + + } + + g_debug ("NSS database '%s' loaded", GSD_SMARTCARD_MANAGER_NSS_DB); + self->nss_context = context; +} + +static void +unload_nss (GsdSmartcardManager *self) +{ + g_debug ("attempting to unload NSS security system with database '%s'", + GSD_SMARTCARD_MANAGER_NSS_DB); + + if (self->nss_context != NULL) { + g_clear_pointer (&self->nss_context, + NSS_ShutdownContext); + g_debug ("NSS database '%s' unloaded", GSD_SMARTCARD_MANAGER_NSS_DB); + } else { + g_debug ("NSS database '%s' already not loaded", GSD_SMARTCARD_MANAGER_NSS_DB); + } +} + +typedef struct +{ + SECMODModule *driver; + GHashTable *smartcards; + int number_of_consecutive_errors; +} WatchSmartcardsOperation; + +static void +on_watch_cancelled (GCancellable *cancellable, + WatchSmartcardsOperation *operation) +{ + SECMOD_CancelWait (operation->driver); +} + +static gboolean +watch_one_event_from_driver (GsdSmartcardManager *self, + WatchSmartcardsOperation *operation, + GCancellable *cancellable, + GError **error) +{ + PK11SlotInfo *card = NULL, *old_card; + CK_SLOT_ID slot_id; + gulong handler_id; + int old_slot_series = -1, slot_series; + + handler_id = g_cancellable_connect (cancellable, + G_CALLBACK (on_watch_cancelled), + operation, + NULL); + + if (handler_id != 0) { + /* Use the non-blocking version of the call as p11-kit, which + * is used on both Fedora and Ubuntu, doesn't support the + * blocking version of the call. + */ + card = SECMOD_WaitForAnyTokenEvent (operation->driver, CKF_DONT_BLOCK, PR_SecondsToInterval (1)); + } + + g_cancellable_disconnect (cancellable, handler_id); + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) { + g_warning ("smartcard event function cancelled"); + return FALSE; + } + + if (card == NULL) { + int error_code; + + error_code = PORT_GetError (); + + if (error_code == SEC_ERROR_NO_EVENT) { + g_usleep (1 * G_USEC_PER_SEC); + + return TRUE; + } + + operation->number_of_consecutive_errors++; + if (operation->number_of_consecutive_errors > 10) { + g_warning ("Got %d consecutive smartcard errors, so giving up.", + operation->number_of_consecutive_errors); + + g_set_error (error, + GSD_SMARTCARD_MANAGER_ERROR, + GSD_SMARTCARD_MANAGER_ERROR_WITH_NSS, + "encountered unexpected error while " + "waiting for smartcard events (error %x)", + error_code); + return FALSE; + } + + g_warning ("Got potentially spurious smartcard event error: %x.", error_code); + + g_usleep (1 * G_USEC_PER_SEC); + return TRUE; + } + operation->number_of_consecutive_errors = 0; + + slot_id = PK11_GetSlotID (card); + slot_series = PK11_GetSlotSeries (card); + + old_card = g_hash_table_lookup (operation->smartcards, GINT_TO_POINTER ((int) slot_id)); + + /* If there is a different card in the slot now than + * there was before, then we need to emit a removed signal + * for the old card + */ + if (old_card != NULL) { + old_slot_series = PK11_GetSlotSeries (old_card); + + if (old_slot_series != slot_series) { + /* Card registered with slot previously is + * different than this card, so update its + * exported state to track the implicit missed + * removal + */ + gsd_smartcard_service_sync_token (self->service, old_card, cancellable); + } + + g_hash_table_remove (operation->smartcards, GINT_TO_POINTER ((int) slot_id)); + } + + if (PK11_IsPresent (card)) { + g_debug ("Detected smartcard insertion event in slot %d", (int) slot_id); + + g_hash_table_replace (operation->smartcards, + GINT_TO_POINTER ((int) slot_id), + PK11_ReferenceSlot (card)); + + gsd_smartcard_service_sync_token (self->service, card, cancellable); + } else if (old_card == NULL) { + /* If the just removed smartcard is not known to us then + * ignore the removal event. NSS sends a synthentic removal + * event for slots that are empty at startup + */ + g_debug ("Detected slot %d is empty in reader", (int) slot_id); + } else { + g_debug ("Detected smartcard removal event in slot %d", (int) slot_id); + + /* If the just removed smartcard is known to us then + * we need to update its exported state to reflect the + * removal + */ + if (old_slot_series == slot_series) + gsd_smartcard_service_sync_token (self->service, card, cancellable); + } + + PK11_FreeSlot (card); + + return TRUE; +} + +static void +watch_smartcards_from_driver (GTask *task, + GsdSmartcardManager *self, + WatchSmartcardsOperation *operation, + GCancellable *cancellable) +{ + g_debug ("watching for smartcard events"); + while (!g_cancellable_is_cancelled (cancellable)) { + gboolean watch_succeeded; + GError *error = NULL; + + watch_succeeded = watch_one_event_from_driver (self, operation, cancellable, &error); + + if (g_task_return_error_if_cancelled (task)) { + break; + } + + if (!watch_succeeded) { + g_task_return_error (task, error); + break; + } + } +} + +static void +destroy_watch_smartcards_operation (WatchSmartcardsOperation *operation) +{ + SECMOD_DestroyModule (operation->driver); + g_hash_table_unref (operation->smartcards); + g_free (operation); +} + +static void +on_smartcards_watch_task_destroyed (GsdSmartcardManager *self, + GTask *freed_task) +{ + G_LOCK (gsd_smartcards_watch_tasks); + self->smartcards_watch_tasks = g_list_remove (self->smartcards_watch_tasks, + freed_task); + G_UNLOCK (gsd_smartcards_watch_tasks); +} + +static void +sync_initial_tokens_from_driver (GsdSmartcardManager *self, + SECMODModule *driver, + GHashTable *smartcards, + GCancellable *cancellable) +{ + int i; + + for (i = 0; i < driver->slotCount; i++) { + PK11SlotInfo *card; + + card = driver->slots[i]; + + if (PK11_IsPresent (card)) { + CK_SLOT_ID slot_id; + slot_id = PK11_GetSlotID (card); + + g_debug ("Detected smartcard in slot %d at start up", (int) slot_id); + + g_hash_table_replace (smartcards, + GINT_TO_POINTER ((int) slot_id), + PK11_ReferenceSlot (card)); + gsd_smartcard_service_sync_token (self->service, card, cancellable); + } + } +} + +static void +watch_smartcards_from_driver_async (GsdSmartcardManager *self, + SECMODModule *driver, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + WatchSmartcardsOperation *operation; + + operation = g_new0 (WatchSmartcardsOperation, 1); + operation->driver = SECMOD_ReferenceModule (driver); + operation->smartcards = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + NULL, + (GDestroyNotify) PK11_FreeSlot); + + task = g_task_new (self, cancellable, callback, user_data); + + g_task_set_task_data (task, + operation, + (GDestroyNotify) destroy_watch_smartcards_operation); + + G_LOCK (gsd_smartcards_watch_tasks); + self->smartcards_watch_tasks = g_list_prepend (self->smartcards_watch_tasks, + task); + g_object_weak_ref (G_OBJECT (task), + (GWeakNotify) on_smartcards_watch_task_destroyed, + self); + G_UNLOCK (gsd_smartcards_watch_tasks); + + sync_initial_tokens_from_driver (self, driver, operation->smartcards, cancellable); + + g_task_run_in_thread (task, (GTaskThreadFunc) watch_smartcards_from_driver); +} + +static gboolean +register_driver_finish (GsdSmartcardManager *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +on_driver_registered (GsdSmartcardManager *self, + GAsyncResult *result, + GTask *task) +{ + GError *error = NULL; + DriverRegistrationOperation *operation; + + operation = g_task_get_task_data (G_TASK (result)); + + if (!register_driver_finish (self, result, &error)) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + watch_smartcards_from_driver_async (self, + operation->driver, + self->cancellable, + (GAsyncReadyCallback) on_smartcards_from_driver_watched, + task); + + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +static void +on_smartcards_from_driver_watched (GsdSmartcardManager *self, + GAsyncResult *result, + GTask *task) +{ + g_debug ("Done watching smartcards from driver"); +} + +static void +destroy_driver_registration_operation (DriverRegistrationOperation *operation) +{ + SECMOD_DestroyModule (operation->driver); + g_free (operation); +} + +static gboolean +on_task_thread_to_complete_driver_registration (GTask *task) +{ + DriverRegistrationOperation *operation; + operation = g_task_get_task_data (task); + + if (operation->error != NULL) + g_task_return_error (task, operation->error); + else + g_task_return_boolean (task, TRUE); + + return G_SOURCE_REMOVE; +} + +static gboolean +on_main_thread_to_register_driver (GTask *task) +{ + GsdSmartcardManager *self; + DriverRegistrationOperation *operation; + GSource *source; + + self = g_task_get_source_object (task); + operation = g_task_get_task_data (task); + + gsd_smartcard_service_register_driver (self->service, + operation->driver); + + source = g_idle_source_new (); + g_task_attach_source (task, + source, + (GSourceFunc) on_task_thread_to_complete_driver_registration); + g_source_unref (source); + + return G_SOURCE_REMOVE; +} + +static void +register_driver (GsdSmartcardManager *self, + SECMODModule *driver, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + DriverRegistrationOperation *operation; + + task = g_task_new (self, cancellable, callback, user_data); + operation = g_new0 (DriverRegistrationOperation, 1); + operation->driver = SECMOD_ReferenceModule (driver); + g_task_set_task_data (task, + operation, + (GDestroyNotify) destroy_driver_registration_operation); + + operation->idle_id = g_idle_add ((GSourceFunc) on_main_thread_to_register_driver, task); + g_source_set_name_by_id (operation->idle_id, "[gnome-settings-daemon] on_main_thread_to_register_driver"); +} + +static void +activate_driver (GsdSmartcardManager *self, + SECMODModule *driver, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_debug ("Activating driver '%s'", driver->commonName); + + task = g_task_new (self, cancellable, callback, user_data); + + register_driver (self, + driver, + cancellable, + (GAsyncReadyCallback) on_driver_registered, + task); +} + +typedef struct +{ + int pending_drivers_count; + int activated_drivers_count; +} ActivateAllDriversOperation; + +static gboolean +activate_driver_async_finish (GsdSmartcardManager *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +try_to_complete_all_drivers_activation (GTask *task) +{ + ActivateAllDriversOperation *operation; + + operation = g_task_get_task_data (task); + + if (operation->pending_drivers_count > 0) + return; + + if (operation->activated_drivers_count > 0) + g_task_return_boolean (task, TRUE); + else + g_task_return_new_error (task, GSD_SMARTCARD_MANAGER_ERROR, + GSD_SMARTCARD_MANAGER_ERROR_NO_DRIVERS, + "No smartcards exist to be activated."); + + g_object_unref (task); +} + +static void +on_driver_activated (GsdSmartcardManager *self, + GAsyncResult *result, + GTask *task) +{ + GError *error = NULL; + gboolean driver_activated; + ActivateAllDriversOperation *operation; + + driver_activated = activate_driver_async_finish (self, result, &error); + + operation = g_task_get_task_data (task); + + if (driver_activated) + operation->activated_drivers_count++; + + operation->pending_drivers_count--; + + try_to_complete_all_drivers_activation (task); +} + +static void +activate_all_drivers_async (GsdSmartcardManager *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + SECMODListLock *lock; + SECMODModuleList *driver_list, *node; + ActivateAllDriversOperation *operation; + + task = g_task_new (self, cancellable, callback, user_data); + operation = g_new0 (ActivateAllDriversOperation, 1); + g_task_set_task_data (task, operation, (GDestroyNotify) g_free); + + lock = SECMOD_GetDefaultModuleListLock (); + + g_assert (lock != NULL); + + SECMOD_GetReadLock (lock); + driver_list = SECMOD_GetDefaultModuleList (); + for (node = driver_list; node != NULL; node = node->next) { + if (!node->module->loaded) + continue; + + if (!SECMOD_HasRemovableSlots (node->module)) + continue; + + if (node->module->dllName == NULL) + continue; + + operation->pending_drivers_count++; + + activate_driver (self, node->module, + cancellable, + (GAsyncReadyCallback) on_driver_activated, + task); + + } + SECMOD_ReleaseReadLock (lock); + + try_to_complete_all_drivers_activation (task); +} + +/* Will error with %GSD_SMARTCARD_MANAGER_ERROR_NO_DRIVERS if there were no + * drivers to activate.. */ +static gboolean +activate_all_drivers_async_finish (GsdSmartcardManager *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +on_all_drivers_activated (GsdSmartcardManager *self, + GAsyncResult *result, + GTask *task) +{ + GError *error = NULL; + gboolean driver_activated; + PK11SlotInfo *login_token; + + driver_activated = activate_all_drivers_async_finish (self, result, &error); + + if (!driver_activated) { + g_task_return_error (task, error); + return; + } + + login_token = gsd_smartcard_manager_get_login_token (self); + + if (login_token || g_getenv ("PKCS11_LOGIN_TOKEN_NAME") != NULL) { + /* The card used to log in was removed before login completed. + * Do removal action immediately + */ + if (!login_token || !PK11_IsPresent (login_token)) + gsd_smartcard_manager_do_remove_action (self); + } + + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +static void +watch_smartcards (GTask *task, + GsdSmartcardManager *self, + gpointer data, + GCancellable *cancellable) +{ + GMainContext *context; + GMainLoop *loop; + + g_debug ("Getting list of suitable drivers"); + context = g_main_context_new (); + g_main_context_push_thread_default (context); + + activate_all_drivers_async (self, + cancellable, + (GAsyncReadyCallback) on_all_drivers_activated, + task); + + loop = g_main_loop_new (context, FALSE); + g_main_loop_run (loop); + g_main_loop_unref (loop); + + g_main_context_pop_thread_default (context); + g_main_context_unref (context); +} + +static void +watch_smartcards_async (GsdSmartcardManager *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + task = g_task_new (self, cancellable, callback, user_data); + + g_task_run_in_thread (task, (GTaskThreadFunc) watch_smartcards); +} + +static gboolean +watch_smartcards_async_finish (GsdSmartcardManager *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +on_smartcards_watched (GsdSmartcardManager *self, + GAsyncResult *result) +{ + GError *error = NULL; + + if (!watch_smartcards_async_finish (self, result, &error)) { + g_debug ("Error watching smartcards: %s", error->message); + g_error_free (error); + } +} + +static void +on_service_created (GObject *source_object, + GAsyncResult *result, + GsdSmartcardManager *self) +{ + GsdSmartcardService *service; + GError *error = NULL; + + service = gsd_smartcard_service_new_finish (result, &error); + + if (service == NULL) { + g_warning("Couldn't create session bus service: %s", error->message); + g_error_free (error); + return; + } + + self->service = service; + + watch_smartcards_async (self, + self->cancellable, + (GAsyncReadyCallback) on_smartcards_watched, + NULL); + +} + +static gboolean +gsd_smartcard_manager_idle_cb (GsdSmartcardManager *self) +{ + gnome_settings_profile_start (NULL); + + self->cancellable = g_cancellable_new(); + self->settings = g_settings_new (CONF_SCHEMA); + + load_nss (self); + + gsd_smartcard_service_new_async (self, + self->cancellable, + (GAsyncReadyCallback) on_service_created, + self); + + gnome_settings_profile_end (NULL); + + self->start_idle_id = 0; + return FALSE; +} + +gboolean +gsd_smartcard_manager_start (GsdSmartcardManager *self, + GError **error) +{ + gnome_settings_profile_start (NULL); + + self->start_idle_id = g_idle_add ((GSourceFunc) gsd_smartcard_manager_idle_cb, self); + g_source_set_name_by_id (self->start_idle_id, "[gnome-settings-daemon] gsd_smartcard_manager_idle_cb"); + + gnome_settings_profile_end (NULL); + + return TRUE; +} + +void +gsd_smartcard_manager_stop (GsdSmartcardManager *self) +{ + g_debug ("Stopping smartcard manager"); + + g_cancellable_cancel (self->cancellable); + + unload_nss (self); + + g_clear_object (&self->settings); + g_clear_object (&self->cancellable); + g_clear_object (&self->session_manager); + g_clear_object (&self->screen_saver); +} + +static void +on_screen_locked (GsdScreenSaver *screen_saver, + GAsyncResult *result, + GsdSmartcardManager *self) +{ + gboolean is_locked; + GError *error = NULL; + + is_locked = gsd_screen_saver_call_lock_finish (screen_saver, result, &error); + + if (!is_locked) { + g_warning ("Couldn't lock screen: %s", error->message); + g_error_free (error); + return; + } +} + +static void +lock_screen (GsdSmartcardManager *self) +{ + if (self->screen_saver == NULL) + self->screen_saver = gnome_settings_bus_get_screen_saver_proxy (); + + gsd_screen_saver_call_lock (self->screen_saver, + self->cancellable, + (GAsyncReadyCallback) on_screen_locked, + self); +} + +static void +on_logged_out (GsdSessionManager *session_manager, + GAsyncResult *result, + GsdSmartcardManager *self) +{ + gboolean is_logged_out; + GError *error = NULL; + + is_logged_out = gsd_session_manager_call_logout_finish (session_manager, result, &error); + + if (!is_logged_out) { + g_warning ("Couldn't log out: %s", error->message); + g_error_free (error); + return; + } +} + +static void +log_out (GsdSmartcardManager *self) +{ + if (self->session_manager == NULL) + self->session_manager = gnome_settings_bus_get_session_proxy (); + + gsd_session_manager_call_logout (self->session_manager, + GSD_SESSION_MANAGER_LOGOUT_MODE_FORCE, + self->cancellable, + (GAsyncReadyCallback) on_logged_out, + self); +} + +void +gsd_smartcard_manager_do_remove_action (GsdSmartcardManager *self) +{ + char *remove_action; + + remove_action = g_settings_get_string (self->settings, KEY_REMOVE_ACTION); + + if (strcmp (remove_action, "lock-screen") == 0) + lock_screen (self); + else if (strcmp (remove_action, "force-logout") == 0) + log_out (self); +} + +static PK11SlotInfo * +get_login_token_for_operation (GsdSmartcardManager *self, + WatchSmartcardsOperation *operation) +{ + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, operation->smartcards); + while (g_hash_table_iter_next (&iter, &key, &value)) { + PK11SlotInfo *card_slot; + const char *token_name; + + card_slot = (PK11SlotInfo *) value; + token_name = PK11_GetTokenName (card_slot); + + if (g_strcmp0 (g_getenv ("PKCS11_LOGIN_TOKEN_NAME"), token_name) == 0) + return card_slot; + } + + return NULL; +} + +PK11SlotInfo * +gsd_smartcard_manager_get_login_token (GsdSmartcardManager *self) +{ + PK11SlotInfo *card_slot = NULL; + GList *node; + + G_LOCK (gsd_smartcards_watch_tasks); + node = self->smartcards_watch_tasks; + while (node != NULL) { + GTask *task = node->data; + WatchSmartcardsOperation *operation = g_task_get_task_data (task); + + card_slot = get_login_token_for_operation (self, operation); + + if (card_slot != NULL) + break; + + node = node->next; + } + G_UNLOCK (gsd_smartcards_watch_tasks); + + return card_slot; +} + +static GList * +get_inserted_tokens_for_operation (GsdSmartcardManager *self, + WatchSmartcardsOperation *operation) +{ + GList *inserted_tokens = NULL; + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, operation->smartcards); + while (g_hash_table_iter_next (&iter, &key, &value)) { + PK11SlotInfo *card_slot; + + card_slot = (PK11SlotInfo *) value; + + if (PK11_IsPresent (card_slot)) + inserted_tokens = g_list_prepend (inserted_tokens, card_slot); + } + + return inserted_tokens; +} + +GList * +gsd_smartcard_manager_get_inserted_tokens (GsdSmartcardManager *self, + gsize *num_tokens) +{ + GList *inserted_tokens = NULL, *node; + + G_LOCK (gsd_smartcards_watch_tasks); + for (node = self->smartcards_watch_tasks; node != NULL; node = node->next) { + GTask *task = node->data; + WatchSmartcardsOperation *operation = g_task_get_task_data (task); + GList *operation_inserted_tokens; + + operation_inserted_tokens = get_inserted_tokens_for_operation (self, operation); + + inserted_tokens = g_list_concat (inserted_tokens, operation_inserted_tokens); + } + G_UNLOCK (gsd_smartcards_watch_tasks); + + if (num_tokens != NULL) + *num_tokens = g_list_length (inserted_tokens); + + return inserted_tokens; +} + +static void +gsd_smartcard_manager_finalize (GObject *object) +{ + GsdSmartcardManager *self; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_SMARTCARD_MANAGER (object)); + + self = GSD_SMARTCARD_MANAGER (object); + + g_return_if_fail (self != NULL); + + if (self->start_idle_id != 0) + g_source_remove (self->start_idle_id); + + gsd_smartcard_manager_stop (self); + + G_OBJECT_CLASS (gsd_smartcard_manager_parent_class)->finalize (object); +} + +GsdSmartcardManager * +gsd_smartcard_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_SMARTCARD_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_SMARTCARD_MANAGER (manager_object); +} diff --git a/plugins/smartcard/gsd-smartcard-manager.h b/plugins/smartcard/gsd-smartcard-manager.h new file mode 100644 index 0000000..c2a9eb3 --- /dev/null +++ b/plugins/smartcard/gsd-smartcard-manager.h @@ -0,0 +1,66 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_SMARTCARD_MANAGER_H +#define __GSD_SMARTCARD_MANAGER_H + +#include <glib-object.h> + +#include <prerror.h> +#include <prinit.h> +#include <nss.h> +#include <pk11func.h> +#include <secmod.h> +#include <secerr.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_SMARTCARD_MANAGER (gsd_smartcard_manager_get_type ()) +#define GSD_SMARTCARD_MANAGER_ERROR (gsd_smartcard_manager_error_quark ()) + +G_DECLARE_FINAL_TYPE (GsdSmartcardManager, gsd_smartcard_manager, GSD, SMARTCARD_MANAGER, GObject) + +typedef enum +{ + GSD_SMARTCARD_MANAGER_ERROR_GENERIC = 0, + GSD_SMARTCARD_MANAGER_ERROR_WITH_NSS, + GSD_SMARTCARD_MANAGER_ERROR_LOADING_DRIVER, + GSD_SMARTCARD_MANAGER_ERROR_WATCHING_FOR_EVENTS, + GSD_SMARTCARD_MANAGER_ERROR_REPORTING_EVENTS, + GSD_SMARTCARD_MANAGER_ERROR_FINDING_SMARTCARD, + GSD_SMARTCARD_MANAGER_ERROR_NO_DRIVERS, +} GsdSmartcardManagerError; + +GQuark gsd_smartcard_manager_error_quark (void); + + +GsdSmartcardManager * gsd_smartcard_manager_new (void); +gboolean gsd_smartcard_manager_start (GsdSmartcardManager *manager, + GError **error); +void gsd_smartcard_manager_stop (GsdSmartcardManager *manager); + +PK11SlotInfo * gsd_smartcard_manager_get_login_token (GsdSmartcardManager *manager); +GList * gsd_smartcard_manager_get_inserted_tokens (GsdSmartcardManager *manager, + gsize *num_tokens); +void gsd_smartcard_manager_do_remove_action (GsdSmartcardManager *manager); + +G_END_DECLS + +#endif /* __GSD_SMARTCARD_MANAGER_H */ diff --git a/plugins/smartcard/gsd-smartcard-service.c b/plugins/smartcard/gsd-smartcard-service.c new file mode 100644 index 0000000..4d529c3 --- /dev/null +++ b/plugins/smartcard/gsd-smartcard-service.c @@ -0,0 +1,849 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2012 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Authors: Ray Strode + */ + +#include "config.h" + +#include "gsd-smartcard-service.h" +#include "org.gnome.SettingsDaemon.Smartcard.h" +#include "gsd-smartcard-manager.h" +#include "gsd-smartcard-enum-types.h" +#include "gsd-smartcard-utils.h" + +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <gio/gio.h> + +struct _GsdSmartcardService +{ + GsdSmartcardServiceManagerSkeleton parent; + + GDBusConnection *bus_connection; + GDBusObjectManagerServer *object_manager_server; + GsdSmartcardManager *smartcard_manager; + GCancellable *cancellable; + GHashTable *tokens; + + gboolean login_token_bound; + guint name_id; +}; + +#define GSD_DBUS_NAME "org.gnome.SettingsDaemon" +#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon" +#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon" + +#define GSD_SMARTCARD_DBUS_NAME GSD_DBUS_NAME ".Smartcard" +#define GSD_SMARTCARD_DBUS_PATH GSD_DBUS_PATH "/Smartcard" +#define GSD_SMARTCARD_MANAGER_DBUS_PATH GSD_SMARTCARD_DBUS_PATH "/Manager" +#define GSD_SMARTCARD_MANAGER_DRIVERS_DBUS_PATH GSD_SMARTCARD_MANAGER_DBUS_PATH "/Drivers" +#define GSD_SMARTCARD_MANAGER_TOKENS_DBUS_PATH GSD_SMARTCARD_MANAGER_DBUS_PATH "/Tokens" + +enum { + PROP_0, + PROP_MANAGER, + PROP_BUS_CONNECTION +}; + +static void gsd_smartcard_service_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *param_spec); +static void gsd_smartcard_service_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *param_spec); +static void async_initable_interface_init (GAsyncInitableIface *interface); +static void smartcard_service_manager_interface_init (GsdSmartcardServiceManagerIface *interface); + +G_LOCK_DEFINE_STATIC (gsd_smartcard_tokens); + +G_DEFINE_TYPE_WITH_CODE (GsdSmartcardService, + gsd_smartcard_service, + GSD_SMARTCARD_SERVICE_TYPE_MANAGER_SKELETON, + G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, + async_initable_interface_init) + G_IMPLEMENT_INTERFACE (GSD_SMARTCARD_SERVICE_TYPE_MANAGER, + smartcard_service_manager_interface_init)); + +static void +set_bus_connection (GsdSmartcardService *self, + GDBusConnection *connection) +{ + if (self->bus_connection != connection) { + g_clear_object (&self->bus_connection); + self->bus_connection = g_object_ref (connection); + g_object_notify (G_OBJECT (self), "bus-connection"); + } +} + +static void +register_object_manager (GsdSmartcardService *self) +{ + GsdSmartcardServiceObjectSkeleton *object; + + self->object_manager_server = g_dbus_object_manager_server_new (GSD_SMARTCARD_DBUS_PATH); + + object = gsd_smartcard_service_object_skeleton_new (GSD_SMARTCARD_MANAGER_DBUS_PATH); + gsd_smartcard_service_object_skeleton_set_manager (object, + GSD_SMARTCARD_SERVICE_MANAGER (self)); + + g_dbus_object_manager_server_export (self->object_manager_server, + G_DBUS_OBJECT_SKELETON (object)); + g_object_unref (object); + + g_dbus_object_manager_server_set_connection (self->object_manager_server, + self->bus_connection); +} + +static const char * +get_login_token_object_path (GsdSmartcardService *self) +{ + return GSD_SMARTCARD_MANAGER_TOKENS_DBUS_PATH "/login_token"; +} + +static void +register_login_token_alias (GsdSmartcardService *self) +{ + GDBusObjectSkeleton *object; + GDBusInterfaceSkeleton *interface; + const char *object_path; + const char *token_name; + + token_name = g_getenv ("PKCS11_LOGIN_TOKEN_NAME"); + + if (token_name == NULL) + return; + + object_path = get_login_token_object_path (self); + object = G_DBUS_OBJECT_SKELETON (gsd_smartcard_service_object_skeleton_new (object_path)); + interface = G_DBUS_INTERFACE_SKELETON (gsd_smartcard_service_token_skeleton_new ()); + + g_dbus_object_skeleton_add_interface (object, interface); + g_object_unref (interface); + + g_object_set (G_OBJECT (interface), + "name", token_name, + "used-to-login", TRUE, + "is-inserted", FALSE, + NULL); + + g_dbus_object_manager_server_export (self->object_manager_server, + object); + + G_LOCK (gsd_smartcard_tokens); + g_hash_table_insert (self->tokens, g_strdup (object_path), interface); + G_UNLOCK (gsd_smartcard_tokens); +} + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *result, + GTask *task) +{ + GsdSmartcardService *self; + GDBusConnection *connection; + GError *error = NULL; + + connection = g_bus_get_finish (result, &error); + if (connection == NULL) { + g_task_return_error (task, error); + goto out; + } + + g_debug ("taking name %s on session bus", GSD_SMARTCARD_DBUS_NAME); + + self = g_task_get_source_object (task); + + set_bus_connection (self, connection); + + register_object_manager (self); + self->name_id = g_bus_own_name_on_connection (connection, + GSD_SMARTCARD_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, + NULL, + NULL, + NULL); + + /* In case the login token is removed at start up, register an + * an alias interface that's always around + */ + register_login_token_alias (self); + g_task_return_boolean (task, TRUE); + +out: + g_object_unref (task); + return; +} + +static gboolean +gsd_smartcard_service_initable_init_finish (GAsyncInitable *initable, + GAsyncResult *result, + GError **error) +{ + GTask *task; + + task = G_TASK (result); + + return g_task_propagate_boolean (task, error); +} + +static void +gsd_smartcard_service_initable_init_async (GAsyncInitable *initable, + int io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsdSmartcardService *self = GSD_SMARTCARD_SERVICE (initable); + GTask *task; + + task = g_task_new (G_OBJECT (self), cancellable, callback, user_data); + g_task_set_priority (task, io_priority); + + g_bus_get (G_BUS_TYPE_SESSION, cancellable, (GAsyncReadyCallback) on_bus_gotten, task); +} + +static void +async_initable_interface_init (GAsyncInitableIface *interface) +{ + interface->init_async = gsd_smartcard_service_initable_init_async; + interface->init_finish = gsd_smartcard_service_initable_init_finish; +} + +static char * +get_object_path_for_token (GsdSmartcardService *self, + PK11SlotInfo *card_slot) +{ + char *object_path; + char *escaped_library_path; + SECMODModule *driver; + CK_SLOT_ID slot_id; + + driver = PK11_GetModule (card_slot); + slot_id = PK11_GetSlotID (card_slot); + + escaped_library_path = gsd_smartcard_utils_escape_object_path (driver->dllName); + + object_path = g_strdup_printf ("%s/token_from_%s_slot_%lu", + GSD_SMARTCARD_MANAGER_TOKENS_DBUS_PATH, + escaped_library_path, + (gulong) slot_id); + g_free (escaped_library_path); + + return object_path; +} + +static gboolean +gsd_smartcard_service_handle_get_login_token (GsdSmartcardServiceManager *manager, + GDBusMethodInvocation *invocation) +{ + GsdSmartcardService *self = GSD_SMARTCARD_SERVICE (manager); + PK11SlotInfo *card_slot; + char *object_path; + + card_slot = gsd_smartcard_manager_get_login_token (self->smartcard_manager); + + if (card_slot == NULL) { + const char *login_token_object_path; + + /* If we know there's a login token but it was removed before the + * smartcard manager could examine it, just return the generic login + * token object path + */ + login_token_object_path = get_login_token_object_path (self); + + if (g_hash_table_contains (self->tokens, login_token_object_path)) { + gsd_smartcard_service_manager_complete_get_login_token (manager, + invocation, + login_token_object_path); + return TRUE; + } + + g_dbus_method_invocation_return_error (invocation, + GSD_SMARTCARD_MANAGER_ERROR, + GSD_SMARTCARD_MANAGER_ERROR_FINDING_SMARTCARD, + _("User was not logged in with smartcard.")); + + return TRUE; + } + + object_path = get_object_path_for_token (self, card_slot); + gsd_smartcard_service_manager_complete_get_login_token (manager, + invocation, + object_path); + g_free (object_path); + + return TRUE; +} + +static gboolean +gsd_smartcard_service_handle_get_inserted_tokens (GsdSmartcardServiceManager *manager, + GDBusMethodInvocation *invocation) +{ + GsdSmartcardService *self = GSD_SMARTCARD_SERVICE (manager); + GList *inserted_tokens, *node; + GPtrArray *object_paths; + + inserted_tokens = gsd_smartcard_manager_get_inserted_tokens (self->smartcard_manager, + NULL); + + object_paths = g_ptr_array_new (); + for (node = inserted_tokens; node != NULL; node = node->next) { + PK11SlotInfo *card_slot = node->data; + char *object_path; + + object_path = get_object_path_for_token (self, card_slot); + g_ptr_array_add (object_paths, object_path); + } + g_ptr_array_add (object_paths, NULL); + g_list_free (inserted_tokens); + + gsd_smartcard_service_manager_complete_get_inserted_tokens (manager, + invocation, + (const char * const *) object_paths->pdata); + + g_ptr_array_free (object_paths, TRUE); + + return TRUE; +} + +static void +smartcard_service_manager_interface_init (GsdSmartcardServiceManagerIface *interface) +{ + interface->handle_get_login_token = gsd_smartcard_service_handle_get_login_token; + interface->handle_get_inserted_tokens = gsd_smartcard_service_handle_get_inserted_tokens; +} + +static void +gsd_smartcard_service_init (GsdSmartcardService *self) +{ + self->tokens = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + NULL); +} + +static void +gsd_smartcard_service_dispose (GObject *object) +{ + GsdSmartcardService *self = GSD_SMARTCARD_SERVICE (object); + + g_clear_object (&self->bus_connection); + g_clear_object (&self->object_manager_server); + g_clear_object (&self->smartcard_manager); + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + g_clear_pointer (&self->tokens, g_hash_table_unref); + + G_OBJECT_CLASS (gsd_smartcard_service_parent_class)->dispose (object); +} + +static void +gsd_smartcard_service_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *param_spec) +{ + GsdSmartcardService *self = GSD_SMARTCARD_SERVICE (object); + + switch (property_id) { + case PROP_MANAGER: + self->smartcard_manager = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, param_spec); + break; + } +} + +static void +gsd_smartcard_service_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *param_spec) +{ + GsdSmartcardService *self = GSD_SMARTCARD_SERVICE (object); + + switch (property_id) { + case PROP_MANAGER: + g_value_set_object (value, self->smartcard_manager); + break; + case PROP_BUS_CONNECTION: + g_value_set_object (value, self->bus_connection); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, param_spec); + break; + } +} + +static void +gsd_smartcard_service_class_init (GsdSmartcardServiceClass *service_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (service_class); + GParamSpec *param_spec; + + object_class->dispose = gsd_smartcard_service_dispose; + object_class->set_property = gsd_smartcard_service_set_property; + object_class->get_property = gsd_smartcard_service_get_property; + + param_spec = g_param_spec_object ("manager", + "Smartcard Manager", + "Smartcard Manager", + GSD_TYPE_SMARTCARD_MANAGER, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + g_object_class_install_property (object_class, PROP_MANAGER, param_spec); + param_spec = g_param_spec_object ("bus-connection", + "Bus Connection", + "bus connection", + G_TYPE_DBUS_CONNECTION, + G_PARAM_READABLE); + g_object_class_install_property (object_class, PROP_BUS_CONNECTION, param_spec); +} + +static void +on_new_async_finished (GObject *source_object, + GAsyncResult *result, + GTask *task) +{ + GError *error = NULL; + GObject *object; + + object = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object), + result, + &error); + + if (object == NULL) { + g_task_return_error (task, error); + goto out; + } + + g_assert (GSD_IS_SMARTCARD_SERVICE (object)); + + g_task_return_pointer (task, object, g_object_unref); +out: + g_object_unref (task); + return; +} + +void +gsd_smartcard_service_new_async (GsdSmartcardManager *manager, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + task = g_task_new (NULL, cancellable, callback, user_data); + + g_async_initable_new_async (GSD_TYPE_SMARTCARD_SERVICE, + G_PRIORITY_DEFAULT, + cancellable, + (GAsyncReadyCallback) on_new_async_finished, + task, + "manager", manager, + NULL); +} + +GsdSmartcardService * +gsd_smartcard_service_new_finish (GAsyncResult *result, + GError **error) +{ + GTask *task; + GsdSmartcardService *self = NULL; + + task = G_TASK (result); + + self = g_task_propagate_pointer (task, error); + + if (self == NULL) + return self; + + return g_object_ref (self); +} + +static char * +get_object_path_for_driver (GsdSmartcardService *self, + SECMODModule *driver) +{ + char *object_path; + char *escaped_library_path; + + escaped_library_path = gsd_smartcard_utils_escape_object_path (driver->dllName); + + object_path = g_build_path ("/", + GSD_SMARTCARD_MANAGER_DRIVERS_DBUS_PATH, + escaped_library_path, NULL); + g_free (escaped_library_path); + + return object_path; +} + +void +gsd_smartcard_service_register_driver (GsdSmartcardService *self, + SECMODModule *driver) +{ + char *object_path; + GDBusObjectSkeleton *object; + GDBusInterfaceSkeleton *interface; + + object_path = get_object_path_for_driver (self, driver); + object = G_DBUS_OBJECT_SKELETON (gsd_smartcard_service_object_skeleton_new (object_path)); + g_free (object_path); + + interface = G_DBUS_INTERFACE_SKELETON (gsd_smartcard_service_driver_skeleton_new ()); + g_dbus_object_skeleton_add_interface (object, interface); + g_object_unref (interface); + + g_object_set (G_OBJECT (interface), + "library", driver->dllName, + "description", driver->commonName, + NULL); + g_dbus_object_manager_server_export (self->object_manager_server, + object); + g_object_unref (object); +} + +static void +synchronize_token_now (GsdSmartcardService *self, + PK11SlotInfo *card_slot) +{ + GDBusInterfaceSkeleton *interface; + char *object_path; + const char *token_name; + gboolean is_present, is_login_card; + + object_path = get_object_path_for_token (self, card_slot); + + G_LOCK (gsd_smartcard_tokens); + interface = g_hash_table_lookup (self->tokens, object_path); + g_free (object_path); + + if (interface == NULL) + goto out; + + token_name = PK11_GetTokenName (card_slot); + is_present = PK11_IsPresent (card_slot); + + if (g_strcmp0 (g_getenv ("PKCS11_LOGIN_TOKEN_NAME"), token_name) == 0) + is_login_card = TRUE; + else + is_login_card = FALSE; + + g_debug ("==============================="); + g_debug (" Token '%s'", token_name); + g_debug (" Inserted: %s", is_present? "yes" : "no"); + g_debug (" Previously used to login: %s", is_login_card? "yes" : "no"); + g_debug ("===============================\n"); + + if (!is_present && is_login_card) { + gboolean was_present; + + g_object_get (G_OBJECT (interface), + "is-inserted", &was_present, + NULL); + + if (was_present) + gsd_smartcard_manager_do_remove_action (self->smartcard_manager); + } + + g_object_set (G_OBJECT (interface), + "used-to-login", is_login_card, + "is-inserted", is_present, + NULL); + g_object_get (G_OBJECT (interface), + "used-to-login", &is_login_card, + "is-inserted", &is_present, + NULL); + + if (is_login_card && !self->login_token_bound) { + const char *login_token_path; + GDBusInterfaceSkeleton *login_token_interface; + + login_token_path = get_login_token_object_path (self); + login_token_interface = g_hash_table_lookup (self->tokens, login_token_path); + + if (login_token_interface != NULL) { + g_object_bind_property (interface, "driver", + login_token_interface, "driver", + G_BINDING_SYNC_CREATE); + g_object_bind_property (interface, "is-inserted", + login_token_interface, "is-inserted", + G_BINDING_SYNC_CREATE); + self->login_token_bound = TRUE; + } + } + +out: + G_UNLOCK (gsd_smartcard_tokens); +} + +typedef struct +{ + PK11SlotInfo *card_slot; + char *object_path; + GSource *main_thread_source; +} RegisterNewTokenOperation; + +static void +destroy_register_new_token_operation (RegisterNewTokenOperation *operation) +{ + g_clear_pointer (&operation->main_thread_source, + g_source_destroy); + PK11_FreeSlot (operation->card_slot); + g_free (operation->object_path); + g_free (operation); +} + +static gboolean +on_main_thread_to_register_new_token (GTask *task) +{ + GsdSmartcardService *self; + GDBusObjectSkeleton *object; + GDBusInterfaceSkeleton *interface; + RegisterNewTokenOperation *operation; + SECMODModule *driver; + char *driver_object_path; + const char *token_name; + + self = g_task_get_source_object (task); + + operation = g_task_get_task_data (task); + operation->main_thread_source = NULL; + + object = G_DBUS_OBJECT_SKELETON (gsd_smartcard_service_object_skeleton_new (operation->object_path)); + interface = G_DBUS_INTERFACE_SKELETON (gsd_smartcard_service_token_skeleton_new ()); + + g_dbus_object_skeleton_add_interface (object, interface); + g_object_unref (interface); + + driver = PK11_GetModule (operation->card_slot); + driver_object_path = get_object_path_for_driver (self, driver); + + token_name = PK11_GetTokenName (operation->card_slot); + + g_object_set (G_OBJECT (interface), + "driver", driver_object_path, + "name", token_name, + NULL); + g_free (driver_object_path); + + g_dbus_object_manager_server_export (self->object_manager_server, + object); + + G_LOCK (gsd_smartcard_tokens); + g_hash_table_insert (self->tokens, g_strdup (operation->object_path), interface); + G_UNLOCK (gsd_smartcard_tokens); + + g_task_return_boolean (task, TRUE); + g_object_unref (task); + + return G_SOURCE_REMOVE; +} + +static void +create_main_thread_source (GSourceFunc callback, + gpointer user_data, + GSource **source_out) +{ + GSource *source; + + source = g_idle_source_new (); + g_source_set_callback (source, callback, user_data, NULL); + + *source_out = source; + g_source_attach (source, NULL); + g_source_unref (source); +} + +static void +register_new_token_in_main_thread (GsdSmartcardService *self, + PK11SlotInfo *card_slot, + char *object_path, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + RegisterNewTokenOperation *operation; + GTask *task; + + operation = g_new0 (RegisterNewTokenOperation, 1); + operation->card_slot = PK11_ReferenceSlot (card_slot); + operation->object_path = g_strdup (object_path); + + task = g_task_new (self, cancellable, callback, user_data); + + g_task_set_task_data (task, + operation, + (GDestroyNotify) destroy_register_new_token_operation); + + create_main_thread_source ((GSourceFunc) on_main_thread_to_register_new_token, + task, + &operation->main_thread_source); +} + +static gboolean +register_new_token_in_main_thread_finish (GsdSmartcardService *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +on_token_registered (GsdSmartcardService *self, + GAsyncResult *result, + PK11SlotInfo *card_slot) +{ + gboolean registered; + GError *error = NULL; + + registered = register_new_token_in_main_thread_finish (self, result, &error); + + if (!registered) { + g_debug ("Couldn't register token: %s", + error->message); + goto out; + } + + synchronize_token_now (self, card_slot); + +out: + PK11_FreeSlot (card_slot); +} + +typedef struct +{ + PK11SlotInfo *card_slot; + GSource *main_thread_source; +} SynchronizeTokenOperation; + +static void +destroy_synchronize_token_operation (SynchronizeTokenOperation *operation) +{ + g_clear_pointer (&operation->main_thread_source, + g_source_destroy); + PK11_FreeSlot (operation->card_slot); + g_free (operation); +} + +static gboolean +on_main_thread_to_synchronize_token (GTask *task) +{ + GsdSmartcardService *self; + SynchronizeTokenOperation *operation; + + self = g_task_get_source_object (task); + + operation = g_task_get_task_data (task); + operation->main_thread_source = NULL; + + synchronize_token_now (self, operation->card_slot); + + g_task_return_boolean (task, TRUE); + g_object_unref (task); + + return G_SOURCE_REMOVE; +} + +static gboolean +synchronize_token_in_main_thread_finish (GsdSmartcardService *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +synchronize_token_in_main_thread (GsdSmartcardService *self, + PK11SlotInfo *card_slot, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SynchronizeTokenOperation *operation; + GTask *task; + + operation = g_new0 (SynchronizeTokenOperation, 1); + operation->card_slot = PK11_ReferenceSlot (card_slot); + + task = g_task_new (self, cancellable, callback, user_data); + + g_task_set_task_data (task, + operation, + (GDestroyNotify) + destroy_synchronize_token_operation); + + create_main_thread_source ((GSourceFunc) + on_main_thread_to_synchronize_token, + task, + &operation->main_thread_source); +} + +static void +on_token_synchronized (GsdSmartcardService *self, + GAsyncResult *result, + PK11SlotInfo *card_slot) +{ + gboolean synchronized; + GError *error = NULL; + + synchronized = synchronize_token_in_main_thread_finish (self, result, &error); + + if (!synchronized) + g_debug ("Couldn't synchronize token: %s", error->message); + + PK11_FreeSlot (card_slot); +} + +void +gsd_smartcard_service_sync_token (GsdSmartcardService *self, + PK11SlotInfo *card_slot, + GCancellable *cancellable) +{ + char *object_path; + GDBusInterfaceSkeleton *interface; + + object_path = get_object_path_for_token (self, card_slot); + + G_LOCK (gsd_smartcard_tokens); + interface = g_hash_table_lookup (self->tokens, object_path); + G_UNLOCK (gsd_smartcard_tokens); + + if (interface == NULL) + register_new_token_in_main_thread (self, + card_slot, + object_path, + cancellable, + (GAsyncReadyCallback) + on_token_registered, + PK11_ReferenceSlot (card_slot)); + + else + synchronize_token_in_main_thread (self, + card_slot, + cancellable, + (GAsyncReadyCallback) + on_token_synchronized, + PK11_ReferenceSlot (card_slot)); + + g_free (object_path); +} diff --git a/plugins/smartcard/gsd-smartcard-service.h b/plugins/smartcard/gsd-smartcard-service.h new file mode 100644 index 0000000..11b3e22 --- /dev/null +++ b/plugins/smartcard/gsd-smartcard-service.h @@ -0,0 +1,60 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2012 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Authors: Ray Strode + */ + +#ifndef __GSD_SMARTCARD_SERVICE_H__ +#define __GSD_SMARTCARD_SERVICE_H__ + +#include <glib.h> +#include <glib-object.h> +#include <gio/gio.h> +#include "gsd-smartcard-manager.h" + +#include "org.gnome.SettingsDaemon.Smartcard.h" + +#include <prerror.h> +#include <prinit.h> +#include <nss.h> +#include <pk11func.h> +#include <secmod.h> +#include <secerr.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_SMARTCARD_SERVICE (gsd_smartcard_service_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdSmartcardService, gsd_smartcard_service, GSD, SMARTCARD_SERVICE, GsdSmartcardServiceManagerSkeleton) + +void gsd_smartcard_service_new_async (GsdSmartcardManager *manager, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GsdSmartcardService *gsd_smartcard_service_new_finish (GAsyncResult *result, + GError **error); + +void gsd_smartcard_service_register_driver (GsdSmartcardService *service, + SECMODModule *driver); +void gsd_smartcard_service_sync_token (GsdSmartcardService *service, + PK11SlotInfo *slot_info, + GCancellable *cancellable); + + +G_END_DECLS + +#endif /* __GSD_SMARTCARD_SERVICE_H__ */ diff --git a/plugins/smartcard/gsd-smartcard-utils.c b/plugins/smartcard/gsd-smartcard-utils.c new file mode 100644 index 0000000..6b9461b --- /dev/null +++ b/plugins/smartcard/gsd-smartcard-utils.c @@ -0,0 +1,174 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" +#include "gsd-smartcard-utils.h" + +#include <string.h> + +#include <glib.h> +#include <gio/gio.h> + +static char * +dashed_string_to_studly_caps (const char *dashed_string) +{ + char *studly_string; + size_t studly_string_length; + size_t i; + + i = 0; + + studly_string = g_strdup (dashed_string); + studly_string_length = strlen (studly_string); + + studly_string[i] = g_ascii_toupper (studly_string[i]); + i++; + + while (i < studly_string_length) { + if (studly_string[i] == '-' || studly_string[i] == '_') { + memmove (studly_string + i, + studly_string + i + 1, + studly_string_length - i - 1); + studly_string_length--; + if (g_ascii_isalpha (studly_string[i])) { + studly_string[i] = g_ascii_toupper (studly_string[i]); + } + } + i++; + } + studly_string[studly_string_length] = '\0'; + + return studly_string; +} + +static char * +dashed_string_to_dbus_error_string (const char *dashed_string, + const char *old_prefix, + const char *new_prefix, + const char *suffix) +{ + char *studly_suffix; + char *dbus_error_string; + size_t dbus_error_string_length; + size_t i; + + i = 0; + + if (g_str_has_prefix (dashed_string, old_prefix) && + (dashed_string[strlen(old_prefix)] == '-' || + dashed_string[strlen(old_prefix)] == '_')) { + dashed_string += strlen (old_prefix) + 1; + } + + studly_suffix = dashed_string_to_studly_caps (suffix); + dbus_error_string = g_strdup_printf ("%s.%s.%s", new_prefix, dashed_string, studly_suffix); + g_free (studly_suffix); + i += strlen (new_prefix) + 1; + + dbus_error_string_length = strlen (dbus_error_string); + + dbus_error_string[i] = g_ascii_toupper (dbus_error_string[i]); + i++; + + while (i < dbus_error_string_length) { + if (dbus_error_string[i] == '_' || dbus_error_string[i] == '-') { + dbus_error_string[i] = '.'; + + if (g_ascii_isalpha (dbus_error_string[i + 1])) { + dbus_error_string[i + 1] = g_ascii_toupper (dbus_error_string[i + 1]); + } + } + + i++; + } + + return dbus_error_string; +} + +void +gsd_smartcard_utils_register_error_domain (GQuark error_domain, + GType error_enum) +{ + const char *error_domain_string; + char *type_name; + GType type; + GTypeClass *type_class; + GEnumClass *enum_class; + guint i; + + error_domain_string = g_quark_to_string (error_domain); + type_name = dashed_string_to_studly_caps (error_domain_string); + type = g_type_from_name (type_name); + type_class = g_type_class_ref (type); + enum_class = G_ENUM_CLASS (type_class); + + for (i = 0; i < enum_class->n_values; i++) { + char *dbus_error_string; + + dbus_error_string = dashed_string_to_dbus_error_string (error_domain_string, + "gsd", + "org.gnome.SettingsDaemon", + enum_class->values[i].value_nick); + + g_debug ("%s: Registering dbus error %s", type_name, dbus_error_string); + g_dbus_error_register_error (error_domain, + enum_class->values[i].value, + dbus_error_string); + g_free (dbus_error_string); + } + + g_type_class_unref (type_class); +} + +char * +gsd_smartcard_utils_escape_object_path (const char *unescaped_string) +{ + const char *p; + char *object_path; + GString *string; + + g_return_val_if_fail (unescaped_string != NULL, NULL); + + string = g_string_new (""); + + for (p = unescaped_string; *p != '\0'; p++) + { + guchar character; + + character = (guchar) * p; + + if (((character >= ((guchar) 'a')) && + (character <= ((guchar) 'z'))) || + ((character >= ((guchar) 'A')) && + (character <= ((guchar) 'Z'))) || + ((character >= ((guchar) '0')) && (character <= ((guchar) '9')))) + { + g_string_append_c (string, (char) character); + continue; + } + + g_string_append_printf (string, "_%x_", character); + } + + object_path = string->str; + + g_string_free (string, FALSE); + + return object_path; +} diff --git a/plugins/smartcard/gsd-smartcard-utils.h b/plugins/smartcard/gsd-smartcard-utils.h new file mode 100644 index 0000000..c7822bf --- /dev/null +++ b/plugins/smartcard/gsd-smartcard-utils.h @@ -0,0 +1,33 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_SMARTCARD_UTILS_H +#define __GSD_SMARTCARD_UTILS_H + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS +void gsd_smartcard_utils_register_error_domain (GQuark error_domain, + GType error_enum); +char * gsd_smartcard_utils_escape_object_path (const char *unescaped_string); + +G_END_DECLS + +#endif /* __GSD_SMARTCARD_MANAGER_H */ diff --git a/plugins/smartcard/main.c b/plugins/smartcard/main.c new file mode 100644 index 0000000..3552e5e --- /dev/null +++ b/plugins/smartcard/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_smartcard_manager_new +#define START gsd_smartcard_manager_start +#define STOP gsd_smartcard_manager_stop +#define MANAGER GsdSmartcardManager +#include "gsd-smartcard-manager.h" + +#include "daemon-skeleton.h" diff --git a/plugins/smartcard/meson.build b/plugins/smartcard/meson.build new file mode 100644 index 0000000..916a0fc --- /dev/null +++ b/plugins/smartcard/meson.build @@ -0,0 +1,49 @@ +sources = files( + 'gsd-smartcard-manager.c', + 'gsd-smartcard-service.c', + 'gsd-smartcard-utils.c', + 'main.c' +) + +enum_headers = files( + 'gsd-smartcard-manager.h', + 'gsd-smartcard-utils.h' +) + +enum_types = 'gsd-smartcard-enum-types' + +sources += gnome.mkenums( + enum_types, + sources: enum_headers, + c_template: enum_types + '.c.in', + h_template: enum_types + '.h.in' +) + +gdbus = 'org.gnome.SettingsDaemon.Smartcard' + +sources += gnome.gdbus_codegen( + gdbus, + gdbus + '.xml', + interface_prefix: gdbus + '.', + namespace: 'GsdSmartcardService', + object_manager: true +) + +deps = plugins_deps + [ + gio_unix_dep, + libnotify_dep, + nss_dep +] + +cflags += ['-DGSD_SMARTCARD_MANAGER_NSS_DB="@0@"'.format(system_nssdb_dir)] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) diff --git a/plugins/smartcard/org.gnome.SettingsDaemon.Smartcard.xml b/plugins/smartcard/org.gnome.SettingsDaemon.Smartcard.xml new file mode 100644 index 0000000..53d56a0 --- /dev/null +++ b/plugins/smartcard/org.gnome.SettingsDaemon.Smartcard.xml @@ -0,0 +1,89 @@ +<!DOCTYPE node PUBLIC + "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> + +<!-- + Copyright (C) 2013 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, see <http://www.gnu.org/licenses/>. + + Author: Ray Strode <rstrode@redhat.com> +--> + +<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> + <!-- + org.gnome.SettingsDaemon.Smartcard.Manager: + + An interface used for managing smartcard functionality. + --> + <interface name="org.gnome.SettingsDaemon.Smartcard.Manager"> + <method name="GetLoginToken"> + <arg name="token" type="o" direction="out"/> + </method> + + <method name="GetInsertedTokens"> + <arg name="tokens" type="ao" direction="out"/> + </method> + </interface> + + <!-- + org.gnome.SettingsDaemon.Smartcard.Driver: + + The smartcard driver interface. + --> + <interface name="org.gnome.SettingsDaemon.Smartcard.Driver"> + <!-- + Library: + Path to PKCS11 module + --> + <property name="Library" type="s" access="read"/> + + <!-- + Description: + String describing the PKCS11 module + --> + <property name="Description" type="s" access="read"/> + </interface> + + <!-- + org.gnome.SettingsDaemon.Smartcard.Token: + + The smartcard interface. + --> + <interface name="org.gnome.SettingsDaemon.Smartcard.Token"> + <!-- + Name: + Name of the token + --> + <property name="Name" type="s" access="read"/> + + <!-- + Driver: + Driver handling token + --> + <property name="Driver" type="o" access="read"/> + + <!-- + IsInserted: + Whether or not the card is inserted + --> + <property name="IsInserted" type="b" access="read"/> + + <!-- + UsedToLogin: + Whether or not the card was used to log in + --> + <property name="UsedToLogin" type="b" access="read"/> + </interface> +</node> diff --git a/plugins/sound/gsd-sound-manager.c b/plugins/sound/gsd-sound-manager.c new file mode 100644 index 0000000..5fdb062 --- /dev/null +++ b/plugins/sound/gsd-sound-manager.c @@ -0,0 +1,362 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Lennart Poettering <lennart@poettering.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <sys/types.h> +#include <sys/wait.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <signal.h> + +#include <locale.h> + +#include <glib/gi18n.h> +#include <gio/gio.h> +#include <pulse/pulseaudio.h> + +#include "gsd-sound-manager.h" +#include "gnome-settings-profile.h" + +struct _GsdSoundManager +{ + GObject parent; + + GSettings *settings; + GList *monitors; + guint timeout; +}; + +static void gsd_sound_manager_class_init (GsdSoundManagerClass *klass); +static void gsd_sound_manager_init (GsdSoundManager *sound_manager); +static void gsd_sound_manager_finalize (GObject *object); + +G_DEFINE_TYPE (GsdSoundManager, gsd_sound_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +static void +sample_info_cb (pa_context *c, const pa_sample_info *i, int eol, void *userdata) +{ + pa_operation *o; + + if (!i) + return; + + g_debug ("Found sample %s", i->name); + + /* We only flush those samples which have an XDG sound name + * attached, because only those originate from themeing */ + if (!(pa_proplist_gets (i->proplist, PA_PROP_EVENT_ID))) + return; + + g_debug ("Dropping sample %s from cache", i->name); + + if (!(o = pa_context_remove_sample (c, i->name, NULL, NULL))) { + g_debug ("pa_context_remove_sample (): %s", pa_strerror (pa_context_errno (c))); + return; + } + + pa_operation_unref (o); + + /* We won't wait until the operation is actually executed to + * speed things up a bit.*/ +} + +static void +flush_cache (void) +{ + pa_mainloop *ml = NULL; + pa_context *c = NULL; + pa_proplist *pl = NULL; + pa_operation *o = NULL; + + g_debug ("Flushing sample cache"); + + if (!(ml = pa_mainloop_new ())) { + g_debug ("Failed to allocate pa_mainloop"); + goto fail; + } + + if (!(pl = pa_proplist_new ())) { + g_debug ("Failed to allocate pa_proplist"); + goto fail; + } + + pa_proplist_sets (pl, PA_PROP_APPLICATION_NAME, PACKAGE_NAME); + pa_proplist_sets (pl, PA_PROP_APPLICATION_VERSION, PACKAGE_VERSION); + pa_proplist_sets (pl, PA_PROP_APPLICATION_ID, "org.gnome.SettingsDaemon.Sound"); + + if (!(c = pa_context_new_with_proplist (pa_mainloop_get_api (ml), PACKAGE_NAME, pl))) { + g_debug ("Failed to allocate pa_context"); + goto fail; + } + + pa_proplist_free (pl); + pl = NULL; + + if (pa_context_connect (c, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL) < 0) { + g_debug ("pa_context_connect(): %s", pa_strerror (pa_context_errno (c))); + goto fail; + } + + /* Wait until the connection is established */ + while (pa_context_get_state (c) != PA_CONTEXT_READY) { + + if (!PA_CONTEXT_IS_GOOD (pa_context_get_state (c))) { + g_debug ("Connection failed: %s", pa_strerror (pa_context_errno (c))); + goto fail; + } + + if (pa_mainloop_iterate (ml, TRUE, NULL) < 0) { + g_debug ("pa_mainloop_iterate() failed"); + goto fail; + } + } + + /* Enumerate all cached samples */ + if (!(o = pa_context_get_sample_info_list (c, sample_info_cb, NULL))) { + g_debug ("pa_context_get_sample_info_list(): %s", pa_strerror (pa_context_errno (c))); + goto fail; + } + + /* Wait until our operation is finished and there's nothing + * more queued to send to the server */ + while (pa_operation_get_state (o) == PA_OPERATION_RUNNING || pa_context_is_pending (c)) { + + if (!PA_CONTEXT_IS_GOOD (pa_context_get_state (c))) { + g_debug ("Connection failed: %s", pa_strerror (pa_context_errno (c))); + goto fail; + } + + if (pa_mainloop_iterate (ml, TRUE, NULL) < 0) { + g_debug ("pa_mainloop_iterate() failed"); + goto fail; + } + } + + g_debug ("Sample cache flushed"); + +fail: + if (o) { + pa_operation_cancel (o); + pa_operation_unref (o); + } + + if (c) { + pa_context_disconnect (c); + pa_context_unref (c); + } + + if (pl) + pa_proplist_free (pl); + + if (ml) + pa_mainloop_free (ml); +} + +static gboolean +flush_cb (GsdSoundManager *manager) +{ + flush_cache (); + manager->timeout = 0; + return FALSE; +} + +static void +trigger_flush (GsdSoundManager *manager) +{ + + if (manager->timeout) + g_source_remove (manager->timeout); + + /* We delay the flushing a bit so that we can coalesce + * multiple changes into a single cache flush */ + manager->timeout = g_timeout_add (500, (GSourceFunc) flush_cb, manager); + g_source_set_name_by_id (manager->timeout, "[gnome-settings-daemon] flush_cb"); +} + +static void +settings_changed_cb (GSettings *settings, + const char *key, + GsdSoundManager *manager) +{ + trigger_flush (manager); +} + +static void +register_config_callback (GsdSoundManager *manager) +{ + manager->settings = g_settings_new ("org.gnome.desktop.sound"); + g_signal_connect (G_OBJECT (manager->settings), "changed", + G_CALLBACK (settings_changed_cb), manager); +} + +static void +file_monitor_changed_cb (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event, + GsdSoundManager *manager) +{ + g_debug ("Theme dir changed"); + trigger_flush (manager); +} + +static gboolean +register_directory_callback (GsdSoundManager *manager, + const char *path, + GError **error) +{ + GFile *f; + GFileMonitor *m; + gboolean succ = FALSE; + + g_debug ("Registering directory monitor for %s", path); + + f = g_file_new_for_path (path); + + m = g_file_monitor_directory (f, 0, NULL, error); + + if (m != NULL) { + g_signal_connect (m, "changed", G_CALLBACK (file_monitor_changed_cb), manager); + + manager->monitors = g_list_prepend (manager->monitors, m); + + succ = TRUE; + } + + g_object_unref (f); + + return succ; +} + +gboolean +gsd_sound_manager_start (GsdSoundManager *manager, + GError **error) +{ + guint i; + const gchar * const * dirs; + char *p; + + g_debug ("Starting sound manager"); + gnome_settings_profile_start (NULL); + + /* We listen for change of the selected theme ... */ + register_config_callback (manager); + + /* ... and we listen to changes of the theme base directories + * in $HOME ...*/ + p = g_build_filename (g_get_user_data_dir (), "sounds", NULL); + if (g_mkdir_with_parents(p, 0700) == 0) + register_directory_callback (manager, p, NULL); + g_free (p); + + /* ... and globally. */ + dirs = g_get_system_data_dirs (); + for (i = 0; dirs[i] != NULL; i++) { + p = g_build_filename (dirs[i], "sounds", NULL); + if (g_file_test (p, G_FILE_TEST_IS_DIR)) + register_directory_callback (manager, p, NULL); + g_free (p); + } + + gnome_settings_profile_end (NULL); + + return TRUE; +} + +void +gsd_sound_manager_stop (GsdSoundManager *manager) +{ + g_debug ("Stopping sound manager"); + + if (manager->settings != NULL) { + g_object_unref (manager->settings); + manager->settings = NULL; + } + + if (manager->timeout) { + g_source_remove (manager->timeout); + manager->timeout = 0; + } + + while (manager->monitors) { + g_file_monitor_cancel (G_FILE_MONITOR (manager->monitors->data)); + g_object_unref (manager->monitors->data); + manager->monitors = g_list_delete_link (manager->monitors, manager->monitors); + } +} + +static void +gsd_sound_manager_dispose (GObject *object) +{ + GsdSoundManager *manager; + + manager = GSD_SOUND_MANAGER (object); + + gsd_sound_manager_stop (manager); + + G_OBJECT_CLASS (gsd_sound_manager_parent_class)->dispose (object); +} + +static void +gsd_sound_manager_class_init (GsdSoundManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gsd_sound_manager_dispose; + object_class->finalize = gsd_sound_manager_finalize; +} + +static void +gsd_sound_manager_init (GsdSoundManager *manager) +{ +} + +static void +gsd_sound_manager_finalize (GObject *object) +{ + GsdSoundManager *sound_manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_SOUND_MANAGER (object)); + + sound_manager = GSD_SOUND_MANAGER (object); + + g_return_if_fail (sound_manager); + + G_OBJECT_CLASS (gsd_sound_manager_parent_class)->finalize (object); +} + +GsdSoundManager * +gsd_sound_manager_new (void) +{ + if (manager_object) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_SOUND_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, (gpointer *) &manager_object); + } + + return GSD_SOUND_MANAGER (manager_object); +} diff --git a/plugins/sound/gsd-sound-manager.h b/plugins/sound/gsd-sound-manager.h new file mode 100644 index 0000000..de25e04 --- /dev/null +++ b/plugins/sound/gsd-sound-manager.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Lennart Poettering <lennart@poettering.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_SOUND_MANAGER_H +#define __GSD_SOUND_MANAGER_H + +#include <glib.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_SOUND_MANAGER (gsd_sound_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdSoundManager, gsd_sound_manager, GSD, SOUND_MANAGER, GObject) + +GsdSoundManager *gsd_sound_manager_new (void); +gboolean gsd_sound_manager_start (GsdSoundManager *manager, GError **error); +void gsd_sound_manager_stop (GsdSoundManager *manager); + +G_END_DECLS + +#endif /* __GSD_SOUND_MANAGER_H */ diff --git a/plugins/sound/main.c b/plugins/sound/main.c new file mode 100644 index 0000000..a170d48 --- /dev/null +++ b/plugins/sound/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_sound_manager_new +#define START gsd_sound_manager_start +#define STOP gsd_sound_manager_stop +#define MANAGER GsdSoundManager +#include "gsd-sound-manager.h" + +#include "daemon-skeleton.h" diff --git a/plugins/sound/meson.build b/plugins/sound/meson.build new file mode 100644 index 0000000..300397f --- /dev/null +++ b/plugins/sound/meson.build @@ -0,0 +1,20 @@ +sources = files( + 'gsd-sound-manager.c', + 'main.c' +) + +deps = plugins_deps + [ + gio_dep, + libpulse_mainloop_glib_dep +] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) diff --git a/plugins/usb-protection/gsd-usb-protection-manager.c b/plugins/usb-protection/gsd-usb-protection-manager.c new file mode 100644 index 0000000..63a4e0d --- /dev/null +++ b/plugins/usb-protection/gsd-usb-protection-manager.c @@ -0,0 +1,1179 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2018 Ludovico de Nittis <denittis@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <gio/gio.h> +#include <glib-object.h> +#include <glib/gi18n.h> +#include <libnotify/notify.h> +#include <locale.h> +#include <string.h> + +#include <gdesktop-enums.h> + +#include "gnome-settings-bus.h" +#include "gnome-settings-profile.h" +#include "gsd-enums.h" +#include "gsd-usb-protection-manager.h" + +#define PRIVACY_SETTINGS "org.gnome.desktop.privacy" +#define USB_PROTECTION "usb-protection" +#define USB_PROTECTION_LEVEL "usb-protection-level" + +#define DBUS_VERSION "1" + +#define USBGUARD_DBUS_NAME "org.usbguard" DBUS_VERSION +#define USBGUARD_DBUS_PATH "/org/usbguard" DBUS_VERSION +#define USBGUARD_DBUS_INTERFACE "org.usbguard" +#define USBGUARD_DBUS_INTERFACE_VERSIONED USBGUARD_DBUS_INTERFACE DBUS_VERSION + +#define USBGUARD_DBUS_PATH_POLICY USBGUARD_DBUS_PATH "/Policy" +#define USBGUARD_DBUS_INTERFACE_POLICY USBGUARD_DBUS_INTERFACE ".Policy" DBUS_VERSION + +#define USBGUARD_DBUS_PATH_DEVICES USBGUARD_DBUS_PATH "/Devices" +#define USBGUARD_DBUS_INTERFACE_DEVICES USBGUARD_DBUS_INTERFACE ".Devices" DBUS_VERSION + +#define APPLY_POLICY "apply-policy" +#define BLOCK "block" +#define REJECT "reject" + +#define APPLY_DEVICE_POLICY "applyDevicePolicy" +#define LIST_DEVICES "listDevices" +#define LIST_RULES "listRules" +#define ALLOW "allow" +#define DEVICE_POLICY_CHANGED "DevicePolicyChanged" +#define DEVICE_PRESENCE_CHANGED "DevicePresenceChanged" +#define INSERTED_DEVICE_POLICY "InsertedDevicePolicy" +#define APPEND_RULE "appendRule" +#define ALLOW_ALL "allow id *:* label \"GNOME_SETTINGS_DAEMON_RULE\"" +#define WITH_CONNECT_TYPE "with-connect-type" +#define WITH_INTERFACE "with-interface" +#define NAME "name" + +struct _GsdUsbProtectionManager +{ + GObject parent; + guint start_idle_id; + GDBusNodeInfo *introspection_data; + GSettings *settings; + guint name_id; + GDBusConnection *connection; + gboolean available; + GDBusProxy *usb_protection; + GDBusProxy *usb_protection_devices; + GDBusProxy *usb_protection_policy; + GCancellable *cancellable; + GsdScreenSaver *screensaver_proxy; + gboolean screensaver_active; + NotifyNotification *notification; +}; + +typedef enum { + EVENT_PRESENT, + EVENT_INSERT, + EVENT_UPDATE, + EVENT_REMOVE +} UsbGuardEvent; + + +typedef enum { + TARGET_ALLOW, + TARGET_BLOCK, + TARGET_REJECT +} UsbGuardTarget; + +typedef enum { + POLICY_DEVICE_ID, + POLICY_TARGET_OLD, + /* This is the rule that has been applied */ + POLICY_TARGET_NEW, + POLICY_DEV_RULE, + /* The ID of the rule that has been applied. + * uint32 - 1 is one of the implicit rules, + * e.g. ImplicitPolicyTarget or InsertedDevicePolicy. + */ + POLICY_RULE_ID, + POLICY_ATTRIBUTES +} UsbGuardPolicyChanged; + +typedef enum { + PRESENCE_DEVICE_ID, + PRESENCE_EVENT, + /* That does not reflect what USBGuard intends to do with the device :( */ + PRESENCE_TARGET, + PRESENCE_DEV_RULE, + PRESENCE_ATTRIBUTES +} UsbGuardPresenceChanged; + +static void gsd_usb_protection_manager_finalize (GObject *object); + +G_DEFINE_TYPE (GsdUsbProtectionManager, gsd_usb_protection_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +#define GSD_DBUS_NAME "org.gnome.SettingsDaemon" +#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon" +#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon" + +#define GSD_USB_PROTECTION_DBUS_NAME GSD_DBUS_NAME ".UsbProtection" +#define GSD_USB_PROTECTION_DBUS_PATH GSD_DBUS_PATH "/UsbProtection" + +static const gchar introspection_xml[] = +"<node>" +" <interface name='org.gnome.SettingsDaemon.UsbProtection'>" +" <property name='Available' type='b' access='read'/>" +" </interface>" +"</node>"; + +static void +dbus_call_log_error (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + const gchar *msg = user_data; + + result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), + res, + &error); + if (result == NULL && + !g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) + g_warning ("%s: %s", msg, error->message); +} + +static void +add_usbguard_allow_rule (GsdUsbProtectionManager *manager) +{ + /* This appends a "allow all" rule. + * It has the purpose of ensuring the authorization of new devices when + * the lockscreen is off while respecting existing rules. + * We make it temporary, so that we are stateless and don't alter the + * existing (persistent) configuration. + */ + + GVariant *params; + GDBusProxy *policy_proxy = manager->usb_protection_policy; + + if (policy_proxy == NULL) { + g_warning ("Cannot add allow rule, because dbus proxy is missing"); + } else { + gboolean temporary = TRUE; + /* This is USBGuard's Rule::LastID */ + /* const guint32 last_rule_id = G_MAXUINT32 - 2; */ + /* We can't use Rule::LastID, due to a bug in USBGuard. + * We cannot pick an arbitrary number, so we pick + * "0" which means we prepend our rule. + * https://github.com/USBGuard/usbguard/pull/355 + */ + const guint32 last_rule_id = 0; + g_debug ("Adding rule %u", last_rule_id); + params = g_variant_new ("(sub)", ALLOW_ALL, last_rule_id, temporary); + g_dbus_proxy_call (policy_proxy, + APPEND_RULE, + params, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + dbus_call_log_error, + "Error appending USBGuard rule"); + } +} + +static gboolean +is_usbguard_allow_rule_present (GVariant *rules) +{ + g_autoptr(GVariantIter) iter = NULL; + g_autofree gchar *value = NULL; + guint number = 0; + + g_debug ("Detecting rule..."); + + g_variant_get (rules, "a(us)", &iter); + while (g_variant_iter_loop (iter, "(us)", &number, &value)) { + if (g_strcmp0 (value, ALLOW_ALL) == 0) { + g_debug ("Detected rule!"); + return TRUE; + } + } + return FALSE; +} + +static void +usbguard_listrules_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GVariant *result, *rules; + g_autoptr(GError) error = NULL; + + result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), + res, + &error); + + if (!result) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_warning ("Failed to fetch USBGuard rules list: %s", error->message); + } + return; + } + + rules = g_variant_get_child_value (result, 0); + g_variant_unref (result); + if (!is_usbguard_allow_rule_present (rules)) + add_usbguard_allow_rule (user_data); + +} + +static void +usbguard_ensure_allow_rule (GsdUsbProtectionManager *manager) +{ + GVariant *params; + GDBusProxy *policy_proxy = manager->usb_protection_policy; + + if (policy_proxy == NULL) { + g_warning ("Cannot list rules, because dbus proxy is missing"); + } else { + /* listRules parameter is a label for matching rules. + * Currently we are using an empty label to get all the + * rules instead of just using "GNOME_SETTINGS_DAEMON_RULE" + * until this bug gets solved: + * https://github.com/USBGuard/usbguard/issues/328 */ + params = g_variant_new ("(s)", ""); + g_dbus_proxy_call (policy_proxy, + LIST_RULES, + params, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + usbguard_listrules_cb, + manager); + } +} + +static void +settings_changed_callback (GSettings *settings, + const char *key, + GsdUsbProtectionManager *manager) +{ + gchar *value_usbguard; + gboolean usbguard_controlled; + GVariant *params; + GDesktopUsbProtection protection_level; + + /* We react only if one of the two USB related properties has been changed */ + if (g_strcmp0 (key, USB_PROTECTION) != 0 && g_strcmp0 (key, USB_PROTECTION_LEVEL) != 0) + return; + + usbguard_controlled = g_settings_get_boolean (settings, USB_PROTECTION); + protection_level = g_settings_get_enum (settings, USB_PROTECTION_LEVEL); + g_debug ("USBGuard control is currently %i with a protection level of %i", + usbguard_controlled, protection_level); + + /* If previously we were controlling USBGuard and now we are not, + * we leave the USBGuard configuration in a clean state. I.e. we set + * "InsertedDevicePolicy" to "apply-policy" and we ensure that + * there is an always allow rule. In this way even if USBGuard daemon + * is running every USB devices will be automatically authorized. */ + if (g_strcmp0 (key, USB_PROTECTION) == 0 && !usbguard_controlled) { + g_debug ("let's clean usbguard config state"); + params = g_variant_new ("(ss)", + INSERTED_DEVICE_POLICY, + APPLY_POLICY); + + if (manager->usb_protection != NULL) { + g_dbus_proxy_call (manager->usb_protection, + "setParameter", + params, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + dbus_call_log_error, + "Error calling USBGuard DBus to set a clean configuration state"); + } + + usbguard_ensure_allow_rule (manager); + } + + /* Only if we are entitled to handle USBGuard */ + if (usbguard_controlled && manager->usb_protection != NULL) { + value_usbguard = (protection_level == G_DESKTOP_USB_PROTECTION_ALWAYS) ? BLOCK : APPLY_POLICY; + params = g_variant_new ("(ss)", + INSERTED_DEVICE_POLICY, + value_usbguard); + + g_dbus_proxy_call (manager->usb_protection, + "setParameter", + params, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + dbus_call_log_error, + "Error calling USBGuard DBus to set the desidered protection level"); + + /* If we are in "When lockscreen is active" we also check if the + * always allow rule is present. */ + if (protection_level == G_DESKTOP_USB_PROTECTION_LOCKSCREEN) + usbguard_ensure_allow_rule (manager); + } +} + +static void update_usb_protection_store (GsdUsbProtectionManager *manager, + GVariant *parameter) +{ + const gchar *key; + gboolean usbguard_controlled; + GDesktopUsbProtection protection_level; + GSettings *settings = manager->settings; + + usbguard_controlled = g_settings_get_boolean (settings, USB_PROTECTION); + /* If we are not handling USBGuard configuration (e.g. the user is using + * a third party program) we do nothing when the config changes. */ + if (usbguard_controlled) { + key = g_variant_get_string (parameter, NULL); + protection_level = g_settings_get_enum (settings, USB_PROTECTION_LEVEL); + /* If the USBGuard configuration has been changed and doesn't match + * our internal state, most likely means that the user externally + * changed it. When this happens we set to false the control value. */ + if ((g_strcmp0 (key, APPLY_POLICY) == 0 && protection_level == G_DESKTOP_USB_PROTECTION_ALWAYS)) { + g_settings_set (settings, USB_PROTECTION, "b", FALSE); + g_warning ("We don't control anymore USBGuard because the configuration changed externally."); + } + } +} + +static gboolean +is_protection_active (GsdUsbProtectionManager *manager) +{ + GSettings *settings = manager->settings; + + return g_settings_get_boolean (settings, USB_PROTECTION); +} + +static void +on_notification_closed (NotifyNotification *n, + GsdUsbProtectionManager *manager) +{ + g_clear_object (&manager->notification); +} + +static void +show_notification (GsdUsbProtectionManager *manager, + const char *summary, + const char *body) +{ + /* Don't show a notice if one is already displayed */ + if (manager->notification != NULL) + return; + + manager->notification = notify_notification_new (summary, body, "drive-removable-media-symbolic"); + notify_notification_set_app_name (manager->notification, _("USB Protection")); + notify_notification_set_hint (manager->notification, "transient", g_variant_new_boolean (TRUE)); + notify_notification_set_hint_string (manager->notification, "x-gnome-privacy-scope", "system"); + notify_notification_set_timeout (manager->notification, NOTIFY_EXPIRES_DEFAULT); + notify_notification_set_urgency (manager->notification, NOTIFY_URGENCY_CRITICAL); + g_signal_connect_object (manager->notification, + "closed", + G_CALLBACK (on_notification_closed), + manager, + 0); + if (!notify_notification_show (manager->notification, NULL)) { + g_warning ("Failed to send USB protection notification"); + g_clear_object (&manager->notification); + } +} + +static void authorize_device (GDBusProxy *proxy, + GsdUsbProtectionManager *manager, + guint device_id, + guint target, + gboolean permanent) +{ + if (manager->usb_protection_devices == NULL) { + g_warning("Could not authorize device, because DBus is missing"); + } else { + GVariant *params = g_variant_new ("(uub)", device_id, target, permanent); + g_dbus_proxy_call (manager->usb_protection_devices, + APPLY_DEVICE_POLICY, + params, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + dbus_call_log_error, + "Error calling USBGuard DBus to authorize a device"); + } +} + +static gboolean +is_hid_or_hub (GVariant *device, + gboolean *has_other_classes) +{ + g_autoptr(GVariantIter) iter = NULL; + g_autofree gchar *name = NULL; + g_autofree gchar *value = NULL; + guint i; + gboolean is_hid_or_hub = FALSE; + + if (has_other_classes != NULL) { + *has_other_classes = FALSE; + } + + g_variant_get_child (device, PRESENCE_ATTRIBUTES, "a{ss}", &iter); + while (g_variant_iter_loop (iter, "{ss}", &name, &value)) { + if (g_strcmp0 (name, WITH_INTERFACE) == 0) { + g_auto(GStrv) interfaces_splitted = NULL; + interfaces_splitted = g_strsplit (value, " ", -1); + for (i = 0; i < g_strv_length (interfaces_splitted); i++) { + if (g_str_has_prefix (interfaces_splitted[i], "03:") + || g_str_has_prefix (interfaces_splitted[i], "09:")) { + is_hid_or_hub = TRUE; + } + else if (has_other_classes != NULL) { + *has_other_classes = TRUE; + } + } + } + } + return is_hid_or_hub; +} + +static gboolean +is_hardwired (GVariant *device) +{ + g_autoptr(GVariantIter) iter = NULL; + g_autofree gchar *name = NULL; + g_autofree gchar *value = NULL; + + g_variant_get_child (device, PRESENCE_ATTRIBUTES, "a{ss}", &iter); + while (g_variant_iter_loop (iter, "{ss}", &name, &value)) { + if (g_strcmp0 (name, WITH_CONNECT_TYPE) == 0) { + return g_strcmp0 (value, "hardwired") == 0; + } + } + return FALSE; +} + +static void +auth_device (GsdUsbProtectionManager *manager, + GVariant *device) +{ + guint device_id; + + if (manager->usb_protection_devices == NULL) + return; + + g_variant_get_child (device, POLICY_DEVICE_ID, "u", &device_id); + authorize_device(manager->usb_protection_devices, + manager, + device_id, + TARGET_ALLOW, + FALSE); +} + +static void +on_screen_locked (GsdScreenSaver *screen_saver, + GAsyncResult *result, + GsdUsbProtectionManager *manager) +{ + g_autoptr(GError) error = NULL; + + gsd_screen_saver_call_lock_finish (screen_saver, result, &error); + + if (error) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + g_warning ("Couldn't lock screen: %s", error->message); + } + + show_notification (manager, + _("New USB device"), + _("New device has been detected while the session was not locked. " + "If you did not plug anything, check your system for any suspicious device.")); + +} + +static void +on_usbguard_signal (GDBusProxy *proxy, + gchar *sender_name, + gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + UsbGuardTarget target = TARGET_BLOCK; + UsbGuardEvent device_event; + GDesktopUsbProtection protection_level; + GsdUsbProtectionManager *manager = user_data; + g_autoptr(GVariantIter) iter = NULL; + g_autofree gchar *name = NULL; + g_autofree gchar *device_name = NULL; + gboolean hid_or_hub = FALSE; + gboolean has_other_classes = FALSE; + + g_debug ("USBGuard signal: %s", signal_name); + + /* We act only if we receive a signal indicating that a device has been inserted */ + if (g_strcmp0 (signal_name, DEVICE_PRESENCE_CHANGED) != 0) { + return; + } + + g_variant_get_child (parameters, PRESENCE_EVENT, "u", &device_event); + if (device_event != EVENT_INSERT) { + g_debug ("Device hat not been inserted (%d); ignoring", device_event); + return; + } + + /* We would like to show a notification for an inserted device that + * *has not been blocked*. But USBGuard is not providing that information. + * So we have to work around that limitation and assume that any device plugged in + * during screensaver shall be blocked. + * https://github.com/USBGuard/usbguard/issues/353 + + g_variant_get_child (parameters, POLICY_TARGET_NEW, "u", &target); + */ + + /* If the device is already authorized we do nothing */ + if (target == TARGET_ALLOW) { + g_debug ("Device will be allowed, we return"); + return; + } + + /* If the USB protection is disabled we do nothing */ + if (!is_protection_active (manager)) { + g_debug ("Protection is not active. Not acting on the device"); + return; + } + + g_variant_get_child (parameters, PRESENCE_ATTRIBUTES, "a{ss}", &iter); + while (g_variant_iter_loop (iter, "{ss}", &name, &device_name)) { + if (g_strcmp0 (name, NAME) == 0) + g_debug ("A new USB device has been connected: %s", device_name); + } + + if (is_hardwired (parameters)) { + g_debug ("Device is hardwired, allowing it to be connected"); + auth_device (manager, parameters); + return; + } + + protection_level = g_settings_get_enum (manager->settings, USB_PROTECTION_LEVEL); + + g_debug ("Screensaver active: %d", manager->screensaver_active); + hid_or_hub = is_hid_or_hub (parameters, &has_other_classes); + if (manager->screensaver_active) { + /* If the session is locked we check if the inserted device is a HID, + * e.g. a keyboard or a mouse, or an HUB. + * If that is the case we authorize the newly inserted device as an + * antilockout policy. + * + * If this device advertises also interfaces outside the HID class, or the + * HUB class, it is suspect. It could be a false positive because this could + * be a "smart" keyboard for example, but at this stage is better be safe. */ + if (hid_or_hub && !has_other_classes) { + show_notification (manager, + _("New device detected"), + _("Either one of your existing devices has been reconnected or a new one has been plugged in. " + "If you did not do it, check your system for any suspicious device.")); + auth_device (manager, parameters); + } else { + if (protection_level == G_DESKTOP_USB_PROTECTION_LOCKSCREEN) { + show_notification (manager, + _("Reconnect USB device"), + _("New device has been detected while you were away. " + "Please disconnect and reconnect the device to start using it.")); + } else { + const char* name_for_notification = device_name ? device_name : "unknown name"; + g_debug ("Showing notification for %s", name_for_notification); + show_notification (manager, + _("USB device blocked"), + _("New device has been detected while you were away. " + "It has been blocked because the USB protection is active.")); + } + } + } else { + /* If the protection level is "lockscreen" the device will be automatically + * authorized by usbguard. */ + if (protection_level == G_DESKTOP_USB_PROTECTION_ALWAYS) { + /* We authorize the device if this is a HID, + * e.g. a keyboard or a mouse, or an HUB. + * We also lock the screen to prevent an attacker to plug malicious + * devices if the legitimate user forgot to lock his session. + * + * If this device advertises also interfaces outside the HID class, or the + * HUB class, it is suspect. It could be a false positive because this could + * be a "smart" keyboard for example, but at this stage is better be safe. */ + if (hid_or_hub && !has_other_classes) { + gsd_screen_saver_call_lock (manager->screensaver_proxy, + manager->cancellable, + (GAsyncReadyCallback) on_screen_locked, + manager); + auth_device (manager, parameters); + } else { + show_notification (manager, + _("USB device blocked"), + _("The new inserted device has been blocked because the USB protection is active.")); + } + } + } +} + +static void +on_usb_protection_signal (GDBusProxy *proxy, + gchar *sender_name, + gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + g_autoptr(GVariant) parameter = NULL; + g_autofree gchar *policy_name = NULL; + + if (g_strcmp0 (signal_name, "PropertyParameterChanged") != 0) + return; + + g_variant_get_child (parameters, 0, "s", &policy_name); + + /* Right now we just care about the InsertedDevicePolicy value */ + if (g_strcmp0 (policy_name, INSERTED_DEVICE_POLICY) != 0) + return; + + parameter = g_variant_get_child_value (parameters, 2); + update_usb_protection_store (user_data, parameter); + +} + +static void +get_parameter_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GVariant *result; + GVariant *params = NULL; + g_autofree gchar *key = NULL; + GDesktopUsbProtection protection_level; + GsdUsbProtectionManager *manager; + GSettings *settings; + g_autoptr(GError) error = NULL; + + result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), + res, + &error); + if (result == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_warning ("Failed to fetch USBGuard parameters: %s", error->message); + } + return; + } + + manager = GSD_USB_PROTECTION_MANAGER (user_data); + settings = manager->settings; + + g_variant_get_child (result, 0, "s", &key); + g_variant_unref (result); + protection_level = g_settings_get_enum (settings, USB_PROTECTION_LEVEL); + + g_debug ("InsertedDevicePolicy is: %s", key); + + if (protection_level == G_DESKTOP_USB_PROTECTION_LOCKSCREEN) { + if (g_strcmp0 (key, APPLY_POLICY) != 0) { + /* We are out of sync. */ + params = g_variant_new ("(ss)", + INSERTED_DEVICE_POLICY, + APPLY_POLICY); + } + } else if (protection_level == G_DESKTOP_USB_PROTECTION_ALWAYS) { + if (g_strcmp0 (key, BLOCK) != 0) { + /* We are out of sync. */ + params = g_variant_new ("(ss)", + INSERTED_DEVICE_POLICY, + BLOCK); + } + } + + if (params != NULL) { + /* We are out of sync. We need to call setParameter to update USBGuard state */ + if (manager->usb_protection != NULL) { + g_debug ("Setting InsertedDevicePolicy"); + g_dbus_proxy_call (manager->usb_protection, + "setParameter", + params, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + dbus_call_log_error, + "Error calling USBGuard DBus while we were out of sync"); + } + + } + + /* If we are in "When lockscreen is active" we also check + * if the "always allow" rule is present. */ + if (protection_level == G_DESKTOP_USB_PROTECTION_LOCKSCREEN) { + g_debug ("Ensuring allow all"); + usbguard_ensure_allow_rule (manager); + } +} + +static void +sync_usb_protection (GDBusProxy *proxy, + GsdUsbProtectionManager *manager) +{ + GVariant *params; + gboolean usbguard_controlled; + GSettings *settings = manager->settings; + + usbguard_controlled = g_settings_get_boolean (settings, USB_PROTECTION); + + g_debug ("Attempting to sync USB parameters: %d %p %p", + usbguard_controlled, proxy, manager->usb_protection); + + if (!usbguard_controlled || manager->usb_protection == NULL) + return; + + params = g_variant_new ("(s)", INSERTED_DEVICE_POLICY); + g_dbus_proxy_call (manager->usb_protection, + "getParameter", + params, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + get_parameter_cb, + manager); +} + +static void +usb_protection_properties_changed (GsdUsbProtectionManager *manager) +{ + GVariantBuilder props_builder; + GVariant *props_changed = NULL; + + /* not yet connected to the session bus */ + if (manager->connection == NULL) + return; + + g_variant_builder_init (&props_builder, G_VARIANT_TYPE ("a{sv}")); + + g_variant_builder_add (&props_builder, "{sv}", "Available", + g_variant_new_boolean (manager->available)); + + props_changed = g_variant_new ("(s@a{sv}@as)", GSD_USB_PROTECTION_DBUS_NAME, + g_variant_builder_end (&props_builder), + g_variant_new_strv (NULL, 0)); + + g_dbus_connection_emit_signal (manager->connection, + NULL, + GSD_USB_PROTECTION_DBUS_PATH, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + props_changed, NULL); +} + +static void +on_usb_protection_owner_changed_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + GsdUsbProtectionManager *manager = user_data; + GDBusProxy *proxy = G_DBUS_PROXY(object); + g_autofree gchar *name_owner = NULL; + + name_owner = g_dbus_proxy_get_name_owner (proxy); + g_debug ("Got owner change: %s", name_owner); + + if (name_owner) { + manager->available = TRUE; + } else { + manager->available = FALSE; + } + + usb_protection_properties_changed (manager); +} + +static void +handle_screensaver_active (GsdUsbProtectionManager *manager, + GVariant *parameters) +{ + gboolean active; + gchar *value_usbguard; + gboolean usbguard_controlled; + GVariant *params; + GDesktopUsbProtection protection_level; + GSettings *settings = manager->settings; + + usbguard_controlled = g_settings_get_boolean (settings, USB_PROTECTION); + protection_level = g_settings_get_enum (settings, USB_PROTECTION_LEVEL); + + g_variant_get (parameters, "(b)", &active); + g_debug ("Received screensaver ActiveChanged signal: %d (old: %d)", active, manager->screensaver_active); + if (manager->screensaver_active != active) { + manager->screensaver_active = active; + if (usbguard_controlled && protection_level == G_DESKTOP_USB_PROTECTION_LOCKSCREEN) { + /* If we are in the "lockscreen protection" level we change + * the usbguard config with apply-policy or block if the session + * is unlocked or locked, respectively. */ + value_usbguard = active ? BLOCK : APPLY_POLICY; + params = g_variant_new ("(ss)", + INSERTED_DEVICE_POLICY, + value_usbguard); + if (manager->usb_protection != NULL) { + g_dbus_proxy_call (manager->usb_protection, + "setParameter", + params, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + dbus_call_log_error, + "Error calling USBGuard DBus to change the protection after a screensaver event"); + } + } + } +} + +static void +screensaver_signal_cb (GDBusProxy *proxy, + const gchar *sender_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + g_debug ("ScreenSaver Signal: %s", signal_name); + if (g_strcmp0 (signal_name, "ActiveChanged") == 0) + handle_screensaver_active (GSD_USB_PROTECTION_MANAGER (user_data), parameters); +} + +static void +usb_protection_policy_proxy_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsdUsbProtectionManager *manager; + GDBusProxy *proxy; + g_autoptr(GError) error = NULL; + g_debug ("usb_protection_policy_proxy_ready"); + + proxy = g_dbus_proxy_new_for_bus_finish (res, &error); + if (!proxy) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to contact USBGuard: %s", error->message); + return; + } else { + manager = GSD_USB_PROTECTION_MANAGER (user_data); + manager->usb_protection_policy = proxy; + g_debug ("Set protection policy proxy to %p", proxy); + sync_usb_protection (proxy, manager); + } +} + +static void +usb_protection_devices_proxy_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsdUsbProtectionManager *manager; + GDBusProxy *proxy; + g_autoptr(GError) error = NULL; + + proxy = g_dbus_proxy_new_for_bus_finish (res, &error); + if (!proxy) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to contact USBGuard: %s", error->message); + return; + } + manager = GSD_USB_PROTECTION_MANAGER (user_data); + manager->usb_protection_devices = proxy; + + /* We don't care about already plugged in devices because they'll be + * already autorized by the "allow all" rule in USBGuard. */ + g_debug ("Listening to signals"); + g_signal_connect_object (source_object, + "g-signal", + G_CALLBACK (on_usbguard_signal), + user_data, + 0); +} + +static void +get_current_screen_saver_status (GsdUsbProtectionManager *manager) +{ + g_autoptr(GVariant) ret = NULL; + g_autoptr(GError) error = NULL; + + ret = g_dbus_proxy_call_sync (G_DBUS_PROXY (manager->screensaver_proxy), + "GetActive", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + manager->cancellable, + &error); + if (ret == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to get screen saver status: %s", error->message); + return; + } + handle_screensaver_active (manager, ret); +} + +static void +usb_protection_proxy_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsdUsbProtectionManager *manager; + GDBusProxy *proxy; + g_autofree gchar *name_owner = NULL; + g_autoptr(GError) error = NULL; + + proxy = g_dbus_proxy_new_for_bus_finish (res, &error); + if (!proxy) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to contact USBGuard: %s", error->message); + return; + } + manager = GSD_USB_PROTECTION_MANAGER (user_data); + manager->usb_protection = proxy; + + g_signal_connect (G_OBJECT (manager->settings), "changed", + G_CALLBACK (settings_changed_callback), manager); + + manager->screensaver_proxy = gnome_settings_bus_get_screen_saver_proxy (); + + get_current_screen_saver_status (manager); + + g_signal_connect (manager->screensaver_proxy, "g-signal", + G_CALLBACK (screensaver_signal_cb), manager); + + name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (proxy)); + + if (name_owner == NULL) { + g_debug("Probably USBGuard >= 0.7.5 is not currently installed."); + manager->available = FALSE; + } else { + manager->available = TRUE; + } + + g_signal_connect_object (source_object, + "notify::g-name-owner", + G_CALLBACK (on_usb_protection_owner_changed_cb), + user_data, + 0); + + g_signal_connect_object (source_object, + "g-signal", + G_CALLBACK (on_usb_protection_signal), + user_data, + 0); + + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + USBGUARD_DBUS_NAME, + USBGUARD_DBUS_PATH_DEVICES, + USBGUARD_DBUS_INTERFACE_DEVICES, + manager->cancellable, + usb_protection_devices_proxy_ready, + manager); + + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + USBGUARD_DBUS_NAME, + USBGUARD_DBUS_PATH_POLICY, + USBGUARD_DBUS_INTERFACE_POLICY, + manager->cancellable, + usb_protection_policy_proxy_ready, + manager); +} + +static GVariant * +handle_get_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, + gpointer user_data) +{ + GsdUsbProtectionManager *manager = GSD_USB_PROTECTION_MANAGER (user_data); + + /* Check session pointer as a proxy for whether the manager is in the + start or stop state */ + if (manager->connection == NULL) + return NULL; + + if (g_strcmp0 (property_name, "Available") == 0) + return g_variant_new_boolean (manager->available); + + return NULL; +} + +static const GDBusInterfaceVTable interface_vtable = +{ + NULL, + handle_get_property, + NULL +}; + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *res, + GsdUsbProtectionManager *manager) +{ + GDBusConnection *connection; + GError *error = NULL; + + connection = g_bus_get_finish (res, &error); + if (connection == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Could not get session bus: %s", error->message); + g_error_free (error); + return; + } + manager->connection = connection; + + g_dbus_connection_register_object (connection, + GSD_USB_PROTECTION_DBUS_PATH, + manager->introspection_data->interfaces[0], + &interface_vtable, + manager, + NULL, + NULL); + + manager->name_id = g_bus_own_name_on_connection (connection, + GSD_USB_PROTECTION_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, + NULL, + NULL, + NULL); +} + +static gboolean +start_usb_protection_idle_cb (GsdUsbProtectionManager *manager) +{ + g_debug ("Starting USB protection manager"); + + manager->settings = g_settings_new (PRIVACY_SETTINGS); + manager->cancellable = g_cancellable_new (); + + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + USBGUARD_DBUS_NAME, + USBGUARD_DBUS_PATH, + USBGUARD_DBUS_INTERFACE_VERSIONED, + manager->cancellable, + usb_protection_proxy_ready, + manager); + + notify_init ("gnome-settings-daemon"); + + manager->start_idle_id = 0; + + return FALSE; +} + +gboolean +gsd_usb_protection_manager_start (GsdUsbProtectionManager *manager, + GError **error) +{ + gnome_settings_profile_start (NULL); + + manager->start_idle_id = g_idle_add ((GSourceFunc) start_usb_protection_idle_cb, manager); + g_source_set_name_by_id (manager->start_idle_id, "[gnome-settings-daemon] start_usbguard_idle_cb"); + + manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + g_assert (manager->introspection_data != NULL); + + /* Start process of owning a D-Bus name */ + g_bus_get (G_BUS_TYPE_SESSION, + manager->cancellable, + (GAsyncReadyCallback) on_bus_gotten, + manager); + + gnome_settings_profile_end (NULL); + return TRUE; +} + +void +gsd_usb_protection_manager_stop (GsdUsbProtectionManager *manager) +{ + g_debug ("Stopping USB protection manager"); + + if (manager->cancellable != NULL) { + g_cancellable_cancel (manager->cancellable); + g_clear_object (&manager->cancellable); + } + + g_clear_object (&manager->notification); + + if (manager->start_idle_id != 0) { + g_source_remove (manager->start_idle_id); + manager->start_idle_id = 0; + } + + if (manager->name_id != 0) { + g_bus_unown_name (manager->name_id); + manager->name_id = 0; + } + + g_clear_pointer (&manager->introspection_data, g_dbus_node_info_unref); + g_clear_object (&manager->connection); + g_clear_object (&manager->settings); + g_clear_object (&manager->usb_protection); + g_clear_object (&manager->usb_protection_devices); + g_clear_object (&manager->usb_protection_policy); + g_clear_object (&manager->screensaver_proxy); +} + +static void +gsd_usb_protection_manager_class_init (GsdUsbProtectionManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_usb_protection_manager_finalize; +} + +static void +gsd_usb_protection_manager_init (GsdUsbProtectionManager *manager) +{ +} + +static void +gsd_usb_protection_manager_finalize (GObject *object) +{ + GsdUsbProtectionManager *usb_protection_manager; + + usb_protection_manager = GSD_USB_PROTECTION_MANAGER (object); + gsd_usb_protection_manager_stop (usb_protection_manager); + + G_OBJECT_CLASS (gsd_usb_protection_manager_parent_class)->finalize (object); +} + +GsdUsbProtectionManager * +gsd_usb_protection_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_USB_PROTECTION_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_USB_PROTECTION_MANAGER (manager_object); +} diff --git a/plugins/usb-protection/gsd-usb-protection-manager.h b/plugins/usb-protection/gsd-usb-protection-manager.h new file mode 100644 index 0000000..8b2bbd7 --- /dev/null +++ b/plugins/usb-protection/gsd-usb-protection-manager.h @@ -0,0 +1,44 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2018 Ludovico de Nittis <denittis@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_USB_PROTECTION_MANAGER_H +#define __GSD_USB_PROTECTION_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_USB_PROTECTION_MANAGER (gsd_usb_protection_manager_get_type ()) +G_DECLARE_FINAL_TYPE (GsdUsbProtectionManager, gsd_usb_protection_manager, GSD, USB_PROTECTION_MANAGER, GObject); + +typedef struct +{ + GObjectClass parent_class; +} _GsdUsbProtectionManagerClass; + +GType gsd_usb_protection_manager_get_type (void); + +GsdUsbProtectionManager * gsd_usb_protection_manager_new (void); +gboolean gsd_usb_protection_manager_start (GsdUsbProtectionManager *manager, + GError **error); +void gsd_usb_protection_manager_stop (GsdUsbProtectionManager *manager); + +G_END_DECLS + +#endif /* __GSD_USB_PROTECTION_MANAGER_H */ diff --git a/plugins/usb-protection/main.c b/plugins/usb-protection/main.c new file mode 100644 index 0000000..fd453bc --- /dev/null +++ b/plugins/usb-protection/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_usb_protection_manager_new +#define START gsd_usb_protection_manager_start +#define STOP gsd_usb_protection_manager_stop +#define MANAGER GsdUsbProtectionManager +#include "gsd-usb-protection-manager.h" + +#include "daemon-skeleton.h" diff --git a/plugins/usb-protection/meson.build b/plugins/usb-protection/meson.build new file mode 100644 index 0000000..1eed2fb --- /dev/null +++ b/plugins/usb-protection/meson.build @@ -0,0 +1,21 @@ +sources = files( + 'gsd-usb-protection-manager.c', + 'main.c' +) + +deps = plugins_deps + [ + gsettings_desktop_dep, + libcommon_dep, + libnotify_dep, +] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc, data_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) diff --git a/plugins/wacom/gsd-wacom-manager.c b/plugins/wacom/gsd-wacom-manager.c new file mode 100644 index 0000000..0364063 --- /dev/null +++ b/plugins/wacom/gsd-wacom-manager.c @@ -0,0 +1,528 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <sys/types.h> +#include <sys/wait.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> + +#include <locale.h> + +#include <gdk/gdk.h> + +#ifdef GDK_WINDOWING_WAYLAND +#include <gdk/gdkwayland.h> +#endif +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#endif + +#if HAVE_WACOM +#include <libwacom/libwacom.h> +#endif + +#include "gsd-enums.h" +#include "gnome-settings-profile.h" +#include "gnome-settings-bus.h" +#include "gsd-wacom-manager.h" +#include "gsd-wacom-oled.h" +#include "gsd-settings-migrate.h" +#include "gsd-input-helper.h" + + +#define UNKNOWN_DEVICE_NOTIFICATION_TIMEOUT 15000 + +#define GSD_DBUS_NAME "org.gnome.SettingsDaemon" +#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon" +#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon" + +#define GSD_WACOM_DBUS_PATH GSD_DBUS_PATH "/Wacom" +#define GSD_WACOM_DBUS_NAME GSD_DBUS_NAME ".Wacom" + +#define LEFT_HANDED_KEY "left-handed" + +static const gchar introspection_xml[] = +"<node name='/org/gnome/SettingsDaemon/Wacom'>" +" <interface name='org.gnome.SettingsDaemon.Wacom'>" +" <method name='SetOLEDLabels'>" +" <arg name='device_path' direction='in' type='s'/>" +" <arg name='labels' direction='in' type='as'/>" +" </method>" +" </interface>" +"</node>"; + +struct _GsdWacomManager +{ + GObject parent; + + guint start_idle_id; + GdkSeat *seat; + guint device_added_id; + + GsdShell *shell_proxy; + + gchar *machine_id; + +#if HAVE_WACOM + WacomDeviceDatabase *wacom_db; +#endif + + /* DBus */ + GDBusNodeInfo *introspection_data; + GDBusConnection *dbus_connection; + GCancellable *dbus_cancellable; + guint dbus_register_object_id; + guint name_id; +}; + +static void gsd_wacom_manager_class_init (GsdWacomManagerClass *klass); +static void gsd_wacom_manager_init (GsdWacomManager *wacom_manager); +static void gsd_wacom_manager_finalize (GObject *object); + +static gboolean is_opaque_tablet (GsdWacomManager *manager, + GdkDevice *device); + +G_DEFINE_TYPE (GsdWacomManager, gsd_wacom_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +static GVariant * +map_tablet_mapping (GVariant *value, GVariant *old_default, GVariant *new_default) +{ + const gchar *mapping; + + mapping = g_variant_get_boolean (value) ? "absolute" : "relative"; + return g_variant_new_string (mapping); +} + +static GVariant * +map_tablet_left_handed (GVariant *value, GVariant *old_default, GVariant *new_default) +{ + const gchar *rotation = g_variant_get_string (value, NULL); + return g_variant_new_boolean (g_strcmp0 (rotation, "half") == 0 || + g_strcmp0 (rotation, "ccw") == 0); +} + +static void +migrate_tablet_settings (GsdWacomManager *manager, + GdkDevice *device) +{ + GsdSettingsMigrateEntry tablet_settings[] = { + { "is-absolute", "mapping", map_tablet_mapping }, + { "keep-aspect", "keep-aspect", NULL }, + { "rotation", "left-handed", map_tablet_left_handed }, + }; + gchar *old_path, *new_path; + const gchar *vendor, *product; + + vendor = gdk_device_get_vendor_id (device); + product = gdk_device_get_product_id (device); + + old_path = g_strdup_printf ("/org/gnome/settings-daemon/peripherals/wacom/%s-usb:%s:%s/", + manager->machine_id, vendor, product); + new_path = g_strdup_printf ("/org/gnome/desktop/peripherals/tablets/%s:%s/", + vendor, product); + + gsd_settings_migrate_check ("org.gnome.settings-daemon.peripherals.wacom.deprecated", + old_path, + "org.gnome.desktop.peripherals.tablet", + new_path, + tablet_settings, G_N_ELEMENTS (tablet_settings)); + + /* Opaque tablets' mapping may be modified by users, so only these + * need migration of settings. + */ + if (is_opaque_tablet (manager, device)) { + GsdSettingsMigrateEntry display_setting[] = { + { "display", "output", NULL }, + }; + + gsd_settings_migrate_check ("org.gnome.desktop.peripherals.tablet.deprecated", + new_path, + "org.gnome.desktop.peripherals.tablet", + new_path, + display_setting, G_N_ELEMENTS (display_setting)); + } + + g_free (old_path); + g_free (new_path); +} + +static void +gsd_wacom_manager_class_init (GsdWacomManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_wacom_manager_finalize; +} + +static gchar * +get_device_path (GdkDevice *device) +{ +#if HAVE_WAYLAND + if (gnome_settings_is_wayland ()) + return g_strdup (gdk_wayland_device_get_node_path (device)); + else +#endif + return xdevice_get_device_node (gdk_x11_device_get_id (device)); +} + +static gboolean +is_opaque_tablet (GsdWacomManager *manager, + GdkDevice *device) +{ + gboolean is_opaque = FALSE; +#if HAVE_WACOM + WacomDevice *wacom_device; + gchar *devpath; + + devpath = get_device_path (device); + wacom_device = libwacom_new_from_path (manager->wacom_db, devpath, + WFALLBACK_GENERIC, NULL); + if (wacom_device) { + WacomIntegrationFlags integration_flags; + + integration_flags = libwacom_get_integration_flags (wacom_device); + is_opaque = (integration_flags & + (WACOM_DEVICE_INTEGRATED_DISPLAY | WACOM_DEVICE_INTEGRATED_SYSTEM)) == 0; + libwacom_destroy (wacom_device); + } + +#endif + return is_opaque; +} + +static GdkDevice * +lookup_device_by_path (GsdWacomManager *manager, + const gchar *path) +{ + GList *devices, *l; + + devices = gdk_seat_get_slaves (manager->seat, + GDK_SEAT_CAPABILITY_ALL); + + for (l = devices; l; l = l->next) { + GdkDevice *device = l->data; + gchar *dev_path = get_device_path (device); + + if (g_strcmp0 (dev_path, path) == 0) { + g_free (dev_path); + return device; + } + + g_free (dev_path); + } + + g_list_free (devices); + + return NULL; +} + +static GSettings * +device_get_settings (GdkDevice *device) +{ + GSettings *settings; + gchar *path; + + path = g_strdup_printf ("/org/gnome/desktop/peripherals/tablets/%s:%s/", + gdk_device_get_vendor_id (device), + gdk_device_get_product_id (device)); + settings = g_settings_new_with_path ("org.gnome.desktop.peripherals.tablet", + path); + g_free (path); + + return settings; +} + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer data) +{ + GsdWacomManager *self = GSD_WACOM_MANAGER (data); + GError *error = NULL; + GdkDevice *device; + + if (g_strcmp0 (method_name, "SetOLEDLabels") == 0) { + gchar *device_path, *label; + gboolean left_handed; + GSettings *settings; + GVariantIter *iter; + gint i = 0; + + g_variant_get (parameters, "(sas)", &device_path, &iter); + device = lookup_device_by_path (self, device_path); + if (!device) { + g_dbus_method_invocation_return_value (invocation, NULL); + return; + } + + settings = device_get_settings (device); + left_handed = g_settings_get_boolean (settings, LEFT_HANDED_KEY); + g_object_unref (settings); + + while (g_variant_iter_loop (iter, "s", &label)) { + if (!set_oled (device_path, left_handed, i, label, &error)) { + g_free (label); + break; + } + i++; + } + + g_variant_iter_free (iter); + + if (error) + g_dbus_method_invocation_return_gerror (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, NULL); + } +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + NULL, /* Get Property */ + NULL, /* Set Property */ +}; + +static void +device_added_cb (GdkSeat *seat, + GdkDevice *device, + GsdWacomManager *manager) +{ + if (gdk_device_get_source (device) == GDK_SOURCE_PEN && + gdk_device_get_device_type (device) == GDK_DEVICE_TYPE_SLAVE) { + migrate_tablet_settings (manager, device); + } +} + +static void +add_devices (GsdWacomManager *manager, + GdkSeatCapabilities capabilities) +{ + GList *devices, *l; + + devices = gdk_seat_get_slaves (manager->seat, capabilities); + for (l = devices; l ; l = l->next) + device_added_cb (manager->seat, l->data, manager); + g_list_free (devices); +} + +static void +set_devicepresence_handler (GsdWacomManager *manager) +{ + GdkSeat *seat; + + seat = gdk_display_get_default_seat (gdk_display_get_default ()); + manager->device_added_id = g_signal_connect (seat, "device-added", + G_CALLBACK (device_added_cb), manager); + manager->seat = seat; +} + +static void +gsd_wacom_manager_init (GsdWacomManager *manager) +{ +#if HAVE_WACOM + manager->wacom_db = libwacom_database_new (); +#endif +} + +static gboolean +gsd_wacom_manager_idle_cb (GsdWacomManager *manager) +{ + gnome_settings_profile_start (NULL); + + set_devicepresence_handler (manager); + + add_devices (manager, GDK_SEAT_CAPABILITY_TABLET_STYLUS); + + gnome_settings_profile_end (NULL); + + manager->start_idle_id = 0; + + return FALSE; +} + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *res, + GsdWacomManager *manager) +{ + GDBusConnection *connection; + GError *error = NULL; + + connection = g_bus_get_finish (res, &error); + + if (connection == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Couldn't get session bus: %s", error->message); + g_error_free (error); + return; + } + + manager->dbus_connection = connection; + manager->dbus_register_object_id = g_dbus_connection_register_object (connection, + GSD_WACOM_DBUS_PATH, + manager->introspection_data->interfaces[0], + &interface_vtable, + manager, + NULL, + &error); + + if (manager->dbus_register_object_id == 0) { + g_warning ("Error registering object: %s", error->message); + g_error_free (error); + return; + } + + manager->name_id = g_bus_own_name_on_connection (connection, + GSD_WACOM_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, + NULL, + NULL, + NULL); +} + +static void +register_manager (GsdWacomManager *manager) +{ + manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + manager->dbus_cancellable = g_cancellable_new (); + g_assert (manager->introspection_data != NULL); + + g_bus_get (G_BUS_TYPE_SESSION, + manager->dbus_cancellable, + (GAsyncReadyCallback) on_bus_gotten, + manager); +} + +static gchar * +get_machine_id (void) +{ + gchar *no_per_machine_file, *machine_id = NULL; + gboolean per_machine; + gsize len; + + no_per_machine_file = g_build_filename (g_get_user_config_dir (), "gnome-settings-daemon", "no-per-machine-config", NULL); + per_machine = !g_file_test (no_per_machine_file, G_FILE_TEST_EXISTS); + g_free (no_per_machine_file); + + if (!per_machine || + (!g_file_get_contents ("/etc/machine-id", &machine_id, &len, NULL) && + !g_file_get_contents ("/var/lib/dbus/machine-id", &machine_id, &len, NULL))) { + return g_strdup ("00000000000000000000000000000000"); + } + + machine_id[len - 1] = '\0'; + return machine_id; +} + +gboolean +gsd_wacom_manager_start (GsdWacomManager *manager, + GError **error) +{ + gnome_settings_profile_start (NULL); + + register_manager (manager_object); + + manager->machine_id = get_machine_id (); + + manager->start_idle_id = g_idle_add ((GSourceFunc) gsd_wacom_manager_idle_cb, manager); + g_source_set_name_by_id (manager->start_idle_id, "[gnome-settings-daemon] gsd_wacom_manager_idle_cb"); + + gnome_settings_profile_end (NULL); + + return TRUE; +} + +void +gsd_wacom_manager_stop (GsdWacomManager *manager) +{ + g_debug ("Stopping wacom manager"); + + g_clear_pointer (&manager->machine_id, g_free); + + if (manager->name_id != 0) { + g_bus_unown_name (manager->name_id); + manager->name_id = 0; + } + + if (manager->dbus_register_object_id) { + g_dbus_connection_unregister_object (manager->dbus_connection, + manager->dbus_register_object_id); + manager->dbus_register_object_id = 0; + } + + if (manager->seat != NULL) { + g_signal_handler_disconnect (manager->seat, manager->device_added_id); + manager->seat = NULL; + } +} + +static void +gsd_wacom_manager_finalize (GObject *object) +{ + GsdWacomManager *wacom_manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_WACOM_MANAGER (object)); + + wacom_manager = GSD_WACOM_MANAGER (object); + + g_return_if_fail (wacom_manager != NULL); + + gsd_wacom_manager_stop (wacom_manager); + + if (wacom_manager->start_idle_id != 0) + g_source_remove (wacom_manager->start_idle_id); + + g_clear_object (&wacom_manager->shell_proxy); + +#if HAVE_WACOM + libwacom_database_destroy (wacom_manager->wacom_db); +#endif + + G_OBJECT_CLASS (gsd_wacom_manager_parent_class)->finalize (object); +} + +GsdWacomManager * +gsd_wacom_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_WACOM_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_WACOM_MANAGER (manager_object); +} diff --git a/plugins/wacom/gsd-wacom-manager.h b/plugins/wacom/gsd-wacom-manager.h new file mode 100644 index 0000000..faef607 --- /dev/null +++ b/plugins/wacom/gsd-wacom-manager.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * Copyright (C) 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_WACOM_MANAGER_H +#define __GSD_WACOM_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_WACOM_MANAGER (gsd_wacom_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdWacomManager, gsd_wacom_manager, GSD, WACOM_MANAGER, GObject) + +GsdWacomManager * gsd_wacom_manager_new (void); +gboolean gsd_wacom_manager_start (GsdWacomManager *manager, + GError **error); +void gsd_wacom_manager_stop (GsdWacomManager *manager); + +G_END_DECLS + +#endif /* __GSD_WACOM_MANAGER_H */ diff --git a/plugins/wacom/gsd-wacom-oled-constants.h b/plugins/wacom/gsd-wacom-oled-constants.h new file mode 100644 index 0000000..e93f744 --- /dev/null +++ b/plugins/wacom/gsd-wacom-oled-constants.h @@ -0,0 +1,41 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Przemo Firszt <przemo@firszt.eu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_WACOM_OLED_CONSTANTS_H +#define __GSD_WACOM_OLED_CONSTANTS_H + +G_BEGIN_DECLS + +typedef enum { + GSD_WACOM_OLED_TYPE_USB, + GSD_WACOM_OLED_TYPE_BLUETOOTH, + GSD_WACOM_OLED_TYPE_RAW_BLUETOOTH +} GsdWacomOledType; + +/* OLED parameters */ +#define OLED_WIDTH 64 /*Width of OLED icon - hardware dependent*/ +#define OLED_HEIGHT 32 /*Height of OLED icon - hardware dependent*/ +#define LABEL_SIZE 30 /*Maximum length of text for OLED icon*/ +#define MAX_TOKEN (LABEL_SIZE >> 1) /*Maximum number of tokens equals half of maximum number of characters*/ +#define MAX_IMAGE_SIZE 1024 /*Size of buffer for storing OLED image*/ +#define MAX_1ST_LINE_LEN 10 /*Maximum number of characters in 1st line of OLED icon*/ + +G_END_DECLS + +#endif /* __GSD_WACOM_OLED_CONSTANTS_H */ diff --git a/plugins/wacom/gsd-wacom-oled-helper.c b/plugins/wacom/gsd-wacom-oled-helper.c new file mode 100644 index 0000000..86f2891 --- /dev/null +++ b/plugins/wacom/gsd-wacom-oled-helper.c @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2012 Przemo Firszt <przemo@firszt.eu> + * + * The code is derived from gsd-wacom-led-helper.c + * written by: + * Copyright (C) 2010-2011 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2012 Bastien Nocera <hadess@hadess.net> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#include <unistd.h> + +#include <stdlib.h> +#include "config.h" + +#include <glib.h> +#include <locale.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <gudev/gudev.h> + +#include "gsd-wacom-oled-constants.h" + +#define USB_PIXELS_PER_BYTE 2 +#define BT_PIXELS_PER_BYTE 8 +#define USB_BUF_LEN OLED_HEIGHT * OLED_WIDTH / USB_PIXELS_PER_BYTE +#define BT_BUF_LEN OLED_WIDTH * OLED_HEIGHT / BT_PIXELS_PER_BYTE + +static void +oled_scramble_icon (guchar *image) +{ + guchar buf[USB_BUF_LEN]; + int x, y, i; + guchar l1, l2, h1, h2; + + for (i = 0; i < USB_BUF_LEN; i++) + buf[i] = image[i]; + + for (y = 0; y < (OLED_HEIGHT / 2); y++) { + for (x = 0; x < (OLED_WIDTH / 2); x++) { + l1 = (0x0F & (buf[OLED_HEIGHT - 1 - x + OLED_WIDTH * y])); + l2 = (0x0F & (buf[OLED_HEIGHT - 1 - x + OLED_WIDTH * y] >> 4)); + h1 = (0xF0 & (buf[OLED_WIDTH - 1 - x + OLED_WIDTH * y] << 4)); + h2 = (0xF0 & (buf[OLED_WIDTH - 1 - x + OLED_WIDTH * y])); + + image[2 * x + OLED_WIDTH * y] = h1 | l1; + image[2 * x + 1 + OLED_WIDTH * y] = h2 | l2; + } + } +} + +static void +oled_bt_scramble_icon (guchar *input_image) +{ + unsigned char image[BT_BUF_LEN]; + unsigned mask; + unsigned s1; + unsigned s2; + unsigned r1 ; + unsigned r2 ; + unsigned r; + unsigned char buf[256]; + int i, w, x, y, z; + + for (i = 0; i < BT_BUF_LEN; i++) + image[i] = input_image[i]; + + for (x = 0; x < 32; x++) { + for (y = 0; y < 8; y++) + buf[(8 * x) + (7 - y)] = image[(8 * x) + y]; + } + + /* Change 76543210 into GECA6420 as required by Intuos4 WL + * HGFEDCBA HFDB7531 + */ + for (x = 0; x < 4; x++) { + for (y = 0; y < 4; y++) { + for (z = 0; z < 8; z++) { + mask = 0x0001; + r1 = 0; + r2 = 0; + i = (x << 6) + (y << 4) + z; + s1 = buf[i]; + s2 = buf[i+8]; + for (w = 0; w < 8; w++) { + r1 |= (s1 & mask); + r2 |= (s2 & mask); + s1 <<= 1; + s2 <<= 1; + mask <<= 2; + } + r = r1 | (r2 << 1); + i = (x << 6) + (y << 4) + (z << 1); + image[i] = 0xFF & r; + image[i+1] = (0xFF00 & r) >> 8; + } + } + } + for (i = 0; i < BT_BUF_LEN; i++) + input_image[i] = image[i]; +} + +static void +gsd_wacom_oled_convert_1_bit (guchar *image) +{ + guchar buf[BT_BUF_LEN]; + guchar b0, b1, b2, b3, b4, b5, b6, b7; + int i; + + for (i = 0; i < BT_BUF_LEN; i++) { + b0 = 0b10000000 & (image[(4 * i) + 0] >> 0); + b1 = 0b01000000 & (image[(4 * i) + 0] << 3); + b2 = 0b00100000 & (image[(4 * i) + 1] >> 2); + b3 = 0b00010000 & (image[(4 * i) + 1] << 1); + b4 = 0b00001000 & (image[(4 * i) + 2] >> 4); + b5 = 0b00000100 & (image[(4 * i) + 2] >> 1); + b6 = 0b00000010 & (image[(4 * i) + 3] >> 6); + b7 = 0b00000001 & (image[(4 * i) + 3] >> 3); + buf[i] = b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7; + } + for (i = 0; i < BT_BUF_LEN; i++) + image[i] = buf[i]; +} + +static int +gsd_wacom_oled_prepare_buf (guchar *image, GsdWacomOledType type) +{ + int len = 0; + + switch (type) { + case GSD_WACOM_OLED_TYPE_USB: + /* Image has to be scrambled for devices connected over USB ... */ + oled_scramble_icon (image); + len = USB_BUF_LEN; + break; + case GSD_WACOM_OLED_TYPE_BLUETOOTH: + /* ... but for bluetooth it has to be converted to 1 bit colour instead of scrambling */ + gsd_wacom_oled_convert_1_bit (image); + len = BT_BUF_LEN; + break; + case GSD_WACOM_OLED_TYPE_RAW_BLUETOOTH: + /* Image has also to be scrambled for devices connected over BT using the raw API ... */ + gsd_wacom_oled_convert_1_bit (image); + len = BT_BUF_LEN; + oled_bt_scramble_icon (image); + break; + default: + g_assert_not_reached (); + } + + return len; +} + +static gboolean +gsd_wacom_oled_helper_write (const gchar *filename, gchar *buffer, GsdWacomOledType type, GError **error) +{ + guchar *image; + gint retval; + gsize length; + gint fd = -1; + gboolean ret = TRUE; + + fd = open (filename, O_WRONLY); + if (fd < 0) { + ret = FALSE; + g_set_error (error, 1, 0, "Failed to open filename: %s", filename); + goto out; + } + + image = g_base64_decode (buffer, &length); + if (length != USB_BUF_LEN) { + ret = FALSE; + g_set_error (error, 1, 0, "Base64 buffer has length of %" G_GSIZE_FORMAT " (expected %i)", length, USB_BUF_LEN); + goto out; + } + if (!image) { + ret = FALSE; + g_set_error (error, 1, 0, "Decoding base64 buffer failed"); + goto out; + } + + length = gsd_wacom_oled_prepare_buf (image, type); + if (!length) { + ret = FALSE; + g_set_error (error, 1, 0, "Invalid image buffer length"); + goto out; + } + + retval = write (fd, image, length); + if (retval != length) { + ret = FALSE; + g_set_error (error, 1, 0, "Writing to %s failed", filename); + } + + g_free (image); +out: + if (fd >= 0) + close (fd); + return ret; +} + +static char * +get_oled_sysfs_path (GUdevDevice *device, + int button_num) +{ + char *status; + char *filename; + + status = g_strdup_printf ("button%d_rawimg", button_num); + filename = g_build_filename (g_udev_device_get_sysfs_path (device), "wacom_led", status, NULL); + g_free (status); + + return filename; +} + +static char * +get_bt_oled_sysfs_path (GUdevDevice *device, int button_num) +{ + char *status; + char *filename; + + status = g_strdup_printf ("/oled%i_img", button_num); + filename = g_build_filename (g_udev_device_get_sysfs_path (device), status, NULL); + g_free (status); + return filename; +} + +static char * +get_bt_oled_filename (GUdevClient *client, GUdevDevice *device, int button_num) +{ + GUdevDevice *hid_dev; + const char *dev_uniq; + GList *hid_list; + GList *element; + const char *dev_hid_uniq; + char *filename = NULL; + + dev_uniq = g_udev_device_get_property (device, "UNIQ"); + + hid_list = g_udev_client_query_by_subsystem (client, "hid"); + element = g_list_first(hid_list); + while (element) { + hid_dev = (GUdevDevice*)element->data; + dev_hid_uniq = g_udev_device_get_property (hid_dev, "HID_UNIQ"); + if (g_strrstr (dev_uniq, dev_hid_uniq)){ + filename = get_bt_oled_sysfs_path (hid_dev, button_num); + g_object_unref (hid_dev); + break; + } + g_object_unref (hid_dev); + element = g_list_next(element); + } + g_list_free(hid_list); + return filename; +} + +static char * +get_oled_sys_path (GUdevClient *client, + GUdevDevice *device, + int button_num, + gboolean usb, + GsdWacomOledType *type) +{ + GUdevDevice *parent; + char *filename = NULL; + + /* check for new unified hid implementation first */ + parent = g_udev_device_get_parent_with_subsystem (device, "hid", NULL); + if (parent) { + filename = get_oled_sysfs_path (parent, button_num); + g_object_unref (parent); + if(g_file_test (filename, G_FILE_TEST_EXISTS)) { + *type = usb ? GSD_WACOM_OLED_TYPE_USB : GSD_WACOM_OLED_TYPE_RAW_BLUETOOTH; + return filename; + } + g_clear_pointer (&filename, g_free); + } + + /* old kernel */ + if (usb) { + parent = g_udev_device_get_parent_with_subsystem (device, "usb", "usb_interface"); + if (!parent) + goto no_parent; + + filename = get_oled_sysfs_path (parent, button_num); + *type = GSD_WACOM_OLED_TYPE_USB; + } else if (g_strrstr (g_udev_device_get_property (device, "DEVPATH"), "bluetooth")) { + parent = g_udev_device_get_parent_with_subsystem (device, "input", NULL); + if (!parent) + goto no_parent; + + filename = get_bt_oled_filename (client, parent, button_num); + *type = GSD_WACOM_OLED_TYPE_BLUETOOTH; + } else { + g_critical ("Not an expected device: '%s'", + g_udev_device_get_device_file (device)); + goto out_err; + } + + g_object_unref (parent); + + return filename; + +no_parent: + g_debug ("Could not find proper parent device for '%s'", + g_udev_device_get_device_file (device)); + +out_err: + return NULL; +} + + +int main (int argc, char **argv) +{ + GOptionContext *context; + GUdevClient *client; + GUdevDevice *device; + int uid, euid; + char *filename; + GError *error = NULL; + const char * const subsystems[] = { "input", NULL }; + int ret = 1; + gboolean usb; + GsdWacomOledType type; + + char *path = NULL; + char *buffer = NULL; + int button_num = -1; + + const GOptionEntry options[] = { + { "path", '\0', 0, G_OPTION_ARG_FILENAME, &path, "Device path for the Wacom device", NULL }, + { "buffer", '\0', 0, G_OPTION_ARG_STRING, &buffer, "Image to set base64 encoded", NULL }, + { "button", '\0', 0, G_OPTION_ARG_INT, &button_num, "Which button icon to set", NULL }, + { NULL} + }; + + /* get calling process */ + uid = getuid (); + euid = geteuid (); + if (uid != 0 || euid != 0) { + g_print ("This program can only be used by the root user\n"); + return 1; + } + + context = g_option_context_new (NULL); + g_option_context_set_summary (context, "GNOME Settings Daemon Wacom OLED Icon Helper"); + g_option_context_add_main_entries (context, options, NULL); + g_option_context_parse (context, &argc, &argv, NULL); + + if (path == NULL || + button_num < 0 || + buffer == NULL) { + char *txt; + + txt = g_option_context_get_help (context, FALSE, NULL); + g_print ("%s", txt); + g_free (txt); + + g_option_context_free (context); + + return 1; + } + g_option_context_free (context); + + client = g_udev_client_new (subsystems); + device = g_udev_client_query_by_device_file (client, path); + if (device == NULL) { + g_critical ("Could not find device '%s' in udev database", path); + goto out; + } + + if (g_udev_device_get_property_as_boolean (device, "ID_INPUT_TABLET") == FALSE && + g_udev_device_get_property_as_boolean (device, "ID_INPUT_TOUCHPAD") == FALSE) { + g_critical ("Device '%s' is not a Wacom tablet", path); + goto out; + } + + if (g_strcmp0 (g_udev_device_get_property (device, "ID_BUS"), "usb") != 0) + usb = FALSE; + else + usb = TRUE; + + filename = get_oled_sys_path (client, device, button_num, usb, &type); + if (!filename) + goto out; + + if (gsd_wacom_oled_helper_write (filename, buffer, type, &error) == FALSE) { + g_critical ("Could not set OLED icon for '%s': %s", path, error->message); + g_error_free (error); + g_free (filename); + goto out; + } + g_free (filename); + + g_debug ("Successfully set OLED icon for '%s', button %d", path, button_num); + + ret = 0; + +out: + g_free (path); + g_free (buffer); + + g_clear_object (&device); + g_clear_object (&client); + + return ret; +} diff --git a/plugins/wacom/gsd-wacom-oled.c b/plugins/wacom/gsd-wacom-oled.c new file mode 100644 index 0000000..4c2daf1 --- /dev/null +++ b/plugins/wacom/gsd-wacom-oled.c @@ -0,0 +1,258 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Przemo Firszt <przemo@firszt.eu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <unistd.h> +#include <math.h> +#include <pango/pango.h> +#include <pango/pangocairo.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "gsd-wacom-oled.h" + +#define MAGIC_BASE64 "base64:" /*Label starting with base64: is treated as already encoded*/ +#define MAGIC_BASE64_LEN strlen(MAGIC_BASE64) + +static void +oled_surface_to_image (guchar *image, + cairo_surface_t *surface) +{ + unsigned char *csurf; + int i, x, y; + unsigned char lo, hi; + + cairo_surface_flush (surface); + csurf = cairo_image_surface_get_data (surface); + i = 0; + for (y = 0; y < OLED_HEIGHT; y++) { + for (x = 0; x < (OLED_WIDTH / 2); x++) { + hi = 0xf0 & csurf[4 * OLED_WIDTH * y + 8 * x + 1]; + lo = 0x0f & (csurf[4 * OLED_WIDTH * y + 8 * x + 5] >> 4); + image[i] = hi | lo; + i++; + } + } +} + +static void +oled_split_text (char *label, + char *line1, + char *line2) +{ + char delimiters[5] = "+-_ "; + char **token; + int token_len[MAX_TOKEN]; + gsize length; + int i; + + if (g_utf8_strlen (label, LABEL_SIZE) <= MAX_1ST_LINE_LEN) { + g_utf8_strncpy (line1, label, MAX_1ST_LINE_LEN); + return; + } + + token = g_strsplit_set (label, delimiters, -1); + + if (g_utf8_strlen (token[0], LABEL_SIZE) > MAX_1ST_LINE_LEN) { + g_utf8_strncpy (line1, label, MAX_1ST_LINE_LEN); + g_utf8_strncpy (line2, label + MAX_1ST_LINE_LEN, LABEL_SIZE - MAX_1ST_LINE_LEN); + return; + } + + for (i = 0; token[i] != NULL; i++) + token_len[i] = g_utf8_strlen (token[i], LABEL_SIZE); + + length = token_len[0]; + i = 0; + while ((length + token_len[i + 1] + 1) <= MAX_1ST_LINE_LEN) { + i++; + length = length + token_len[i] + 1; + } + + g_utf8_strncpy (line1, label, length); + g_utf8_strncpy (line2, label + length + 1, LABEL_SIZE - length); + + return; +} + +static void +oled_render_text (char *label, + guchar *image, + gboolean left_handed) +{ + cairo_t *cr; + cairo_surface_t *surface; + PangoFontDescription *desc; + PangoLayout *layout; + int width, height; + double dx, dy; + char line1[LABEL_SIZE + 1] = ""; + char line2[LABEL_SIZE + 1] = ""; + char *buf; + + oled_split_text (label ,line1, line2); + + buf = g_strdup_printf ("%s\n%s", line1, line2); + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, OLED_WIDTH, OLED_HEIGHT); + cr = cairo_create (surface); + + /* Rotate text so it's seen correctly on the device, or at + * least from top to bottom for LTR text in vertical modes. + */ + if (left_handed) { + cairo_translate (cr, OLED_WIDTH, OLED_HEIGHT); + cairo_scale (cr, -1, -1); + } + + cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 0.99); + cairo_paint (cr); + + layout = pango_cairo_create_layout (cr); + pango_layout_set_alignment (layout, PANGO_ALIGN_CENTER); + pango_layout_set_text (layout, buf, - 1); + g_free (buf); + desc = pango_font_description_new (); + + pango_font_description_set_family (desc, "Terminal"); + pango_font_description_set_absolute_size (desc, PANGO_SCALE * 11); + pango_layout_set_font_description (layout, desc); + pango_font_description_free (desc); + pango_layout_get_size (layout, &width, &height); + width = width/PANGO_SCALE; + cairo_new_path (cr); + + dx = trunc (((double)OLED_WIDTH - width) / 2); + + if (*line2 == '\0') + dy = 10; + else + dy = 4; + + cairo_move_to (cr, dx, dy); + cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 1.0); + pango_cairo_update_layout (cr, layout); + + pango_cairo_layout_path (cr, layout); + cairo_fill (cr); + + oled_surface_to_image (image, surface); + + g_object_unref (layout); + cairo_destroy (cr); + + cairo_surface_destroy (surface); +} + +char * +gsd_wacom_oled_gdkpixbuf_to_base64 (GdkPixbuf *pixbuf) +{ + int i, x, y, ch, rs; + guchar *pix, *p; + guchar *image; + guchar lo, hi; + char *base_string, *base64; + + if (OLED_WIDTH != gdk_pixbuf_get_width (pixbuf)) + return NULL; + + if (OLED_HEIGHT != gdk_pixbuf_get_height (pixbuf)) + return NULL; + + ch = gdk_pixbuf_get_n_channels (pixbuf); + rs = gdk_pixbuf_get_rowstride (pixbuf); + pix = gdk_pixbuf_get_pixels (pixbuf); + + image = g_malloc (MAX_IMAGE_SIZE); + i = 0; + for (y = 0; y < OLED_HEIGHT; y++) { + for (x = 0; x < (OLED_WIDTH / 2); x++) { + p = pix + y * rs + 2 * x * ch; + hi = 0xf0 & ((p[0] + p[1] + p[2])/ 3 * p[3] / 0xff); + p = pix + y * rs + ((2 * x) + 1) * ch; + lo = 0x0f & (((p[0] + p[1] + p[2])/ 3 * p[3] / 0xff) >> 4); + image[i] = hi | lo; + i++; + } + } + + base_string = g_base64_encode (image, MAX_IMAGE_SIZE); + base64 = g_strconcat (MAGIC_BASE64, base_string, NULL); + g_free (base_string); + g_free (image); + + return base64; +} + +static char * +oled_encode_image (char *label, + gboolean left_handed) +{ + unsigned char *image; + + image = g_malloc (MAX_IMAGE_SIZE); + + /* convert label to image */ + oled_render_text (label, image, left_handed); + + return (g_base64_encode (image, MAX_IMAGE_SIZE)); +} + +gboolean +set_oled (const gchar *device_path, + gboolean left_handed, + guint button, + char *label, + GError **error) +{ + char *command; + gboolean ret; + char *buffer; + gint status; + +#ifndef HAVE_GUDEV + /* Not implemented on non-Linux systems */ + return TRUE; +#endif + + if (g_str_has_prefix (label, MAGIC_BASE64)) { + buffer = g_strdup (label + MAGIC_BASE64_LEN); + } else { + buffer = oled_encode_image (label, left_handed); + } + + g_debug ("Setting OLED label '%s' on button %d (device %s)", label, button, device_path); + + command = g_strdup_printf ("pkexec " LIBEXECDIR "/gsd-wacom-oled-helper --path %s --button %d --buffer %s", + device_path, button, buffer); + ret = g_spawn_command_line_sync (command, + NULL, + NULL, + &status, + error); + + if (ret == TRUE && status != 0) + ret = FALSE; + + g_free (command); + + return ret; +} diff --git a/plugins/wacom/gsd-wacom-oled.h b/plugins/wacom/gsd-wacom-oled.h new file mode 100644 index 0000000..dc5d39c --- /dev/null +++ b/plugins/wacom/gsd-wacom-oled.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2013 Przemo Firszt <przemo@firszt.eu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "gsd-wacom-oled-constants.h" + +#include <gdk-pixbuf/gdk-pixbuf.h> + +#ifndef __GSD_WACOM_OLED_H +#define __GSD_WACOM_OLED_H + +G_BEGIN_DECLS + +gboolean set_oled (const gchar *device_path, gboolean left_handed, guint button, char *label, GError **error); +char *gsd_wacom_oled_gdkpixbuf_to_base64 (GdkPixbuf *pixbuf); + +G_END_DECLS + +#endif /* __GSD_WACOM_OLED_H */ diff --git a/plugins/wacom/main.c b/plugins/wacom/main.c new file mode 100644 index 0000000..a19a71b --- /dev/null +++ b/plugins/wacom/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_wacom_manager_new +#define START gsd_wacom_manager_start +#define STOP gsd_wacom_manager_stop +#define MANAGER GsdWacomManager +#include "gsd-wacom-manager.h" + +#include "daemon-skeleton-gtk.h" diff --git a/plugins/wacom/meson.build b/plugins/wacom/meson.build new file mode 100644 index 0000000..d8dd0ec --- /dev/null +++ b/plugins/wacom/meson.build @@ -0,0 +1,63 @@ +policy = 'org.gnome.settings-daemon.plugins.wacom.policy' + +policy_in = configure_file( + input: policy + '.in.in', + output: policy + '.in', + configuration: plugins_conf +) + +i18n.merge_file( + policy, + input: policy_in, + output: policy, + po_dir: po_dir, + install: true, + install_dir: join_paths(gsd_datadir, 'polkit-1', 'actions') +) + +sources = files( + 'gsd-wacom-manager.c', + 'gsd-wacom-oled.c', + 'main.c' +) + +deps = plugins_deps + [ + gtk_dep, + libcommon_dep, + m_dep, + pango_dep +] + +if enable_wacom + deps += libwacom_dep +endif + +cflags += ['-DLIBEXECDIR="@0@"'.format(gsd_libexecdir)] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, data_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) + +if enable_gudev + deps = [ + gudev_dep, + m_dep + ] + + executable( + 'gsd-wacom-oled-helper', + 'gsd-wacom-oled-helper.c', + include_directories: top_inc, + dependencies: deps, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir + ) +endif diff --git a/plugins/wacom/org.gnome.settings-daemon.plugins.wacom.policy.in.in b/plugins/wacom/org.gnome.settings-daemon.plugins.wacom.policy.in.in new file mode 100644 index 0000000..fe0df93 --- /dev/null +++ b/plugins/wacom/org.gnome.settings-daemon.plugins.wacom.policy.in.in @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE policyconfig PUBLIC + "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" + "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd"> +<policyconfig> + + <!-- + Policy definitions for gnome-settings-daemon system-wide actions. + --> + + <vendor>GNOME Settings Daemon</vendor> + <vendor_url>http://git.gnome.org/browse/gnome-settings-daemon</vendor_url> + <icon_name>input-tablet</icon_name> + + <action id="org.gnome.settings-daemon.plugins.wacom.wacom-led-helper"> + <!-- SECURITY: + - A normal active user on the local machine does not need permission + to change the LED setting for a Wacom tablet + --> + <description>Modify the lit LED for a Wacom tablet</description> + <message>Authentication is required to modify the lit LED for a Wacom tablet</message> + <defaults> + <allow_any>no</allow_any> + <allow_inactive>no</allow_inactive> + <allow_active>yes</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.exec.path">@libexecdir@/gsd-wacom-led-helper</annotate> + </action> + + <action id="org.gnome.settings-daemon.plugins.wacom.wacom-oled-helper"> + <!-- SECURITY: + - A normal active user on the local machine does not need permission + to change the OLED images for a Wacom tablet + --> + <description>Modify the OLED image for a Wacom tablet</description> + <message>Authentication is required to modify the OLED image for a Wacom tablet</message> + <defaults> + <allow_any>no</allow_any> + <allow_inactive>no</allow_inactive> + <allow_active>yes</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.exec.path">@libexecdir@/gsd-wacom-oled-helper</annotate> + </action> + +</policyconfig> + diff --git a/plugins/wwan/cc-wwan-device.c b/plugins/wwan/cc-wwan-device.c new file mode 100644 index 0000000..57b869b --- /dev/null +++ b/plugins/wwan/cc-wwan-device.c @@ -0,0 +1,1343 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* cc-wwan-device.c + * + * Copyright 2019-2020 Purism SPC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "cc-wwan-device" + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <glib/gi18n.h> +#include <polkit/polkit.h> +#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK) +# include <NetworkManager.h> +# include <nma-mobile-providers.h> +#endif + +#include "cc-wwan-errors-private.h" +#include "cc-wwan-device.h" + +/** + * @short_description: Device Object + * @include: "cc-wwan-device.h" + */ + +struct _CcWwanDevice +{ + GObject parent_instance; + + MMObject *mm_object; + MMModem *modem; + MMSim *sim; + MMModem3gpp *modem_3gpp; + + const char *operator_code; /* MCCMNC */ + GError *error; + + /* Building with NetworkManager is optional, + * so #NMclient type can’t be used here. + */ + GObject *nm_client; /* An #NMClient */ + CcWwanData *wwan_data; + + gulong modem_3gpp_id; + gulong modem_3gpp_locks_id; + + /* Enabled locks like PIN, PIN2, PUK, etc. */ + MMModem3gppFacility locks; + + CcWwanState registration_state; + gboolean network_is_manual; +}; + +G_DEFINE_TYPE (CcWwanDevice, cc_wwan_device, G_TYPE_OBJECT) + + +enum { + PROP_0, + PROP_OPERATOR_NAME, + PROP_ENABLED_LOCKS, + PROP_ERROR, + PROP_HAS_DATA, + PROP_NETWORK_MODE, + PROP_REGISTRATION_STATE, + PROP_SIGNAL, + PROP_UNLOCK_REQUIRED, + N_PROPS +}; + +static GParamSpec *properties[N_PROPS]; + +static void +cc_wwan_device_state_changed_cb (CcWwanDevice *self) +{ + MMModem3gppRegistrationState state; + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_OPERATOR_NAME]); + + state = mm_modem_3gpp_get_registration_state (self->modem_3gpp); + + switch (state) + { + case MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN: + self->registration_state = CC_WWAN_REGISTRATION_STATE_UNKNOWN; + break; + + case MM_MODEM_3GPP_REGISTRATION_STATE_DENIED: + self->registration_state = CC_WWAN_REGISTRATION_STATE_DENIED; + break; + + case MM_MODEM_3GPP_REGISTRATION_STATE_IDLE: + self->registration_state = CC_WWAN_REGISTRATION_STATE_IDLE; + break; + + case MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING: + self->registration_state = CC_WWAN_REGISTRATION_STATE_SEARCHING; + break; + + case MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING: + self->registration_state = CC_WWAN_REGISTRATION_STATE_ROAMING; + break; + + default: + self->registration_state = CC_WWAN_REGISTRATION_STATE_REGISTERED; + break; + } + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_REGISTRATION_STATE]); +} + +static void +cc_wwan_device_locks_changed_cb (CcWwanDevice *self) +{ + self->locks = mm_modem_3gpp_get_enabled_facility_locks (self->modem_3gpp); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENABLED_LOCKS]); +} + +static void +cc_wwan_device_3gpp_changed_cb (CcWwanDevice *self) +{ + gulong handler_id = 0; + + if (self->modem_3gpp_id) + g_signal_handler_disconnect (self->modem_3gpp, self->modem_3gpp_id); + self->modem_3gpp_id = 0; + + if (self->modem_3gpp_locks_id) + g_signal_handler_disconnect (self->modem_3gpp, self->modem_3gpp_locks_id); + self->modem_3gpp_locks_id = 0; + + g_clear_object (&self->modem_3gpp); + self->modem_3gpp = mm_object_get_modem_3gpp (self->mm_object); + + if (self->modem_3gpp) + { + handler_id = g_signal_connect_object (self->modem_3gpp, "notify::registration-state", + G_CALLBACK (cc_wwan_device_state_changed_cb), + self, G_CONNECT_SWAPPED); + self->modem_3gpp_id = handler_id; + + handler_id = g_signal_connect_object (self->modem_3gpp, "notify::enabled-facility-locks", + G_CALLBACK (cc_wwan_device_locks_changed_cb), + self, G_CONNECT_SWAPPED); + self->modem_3gpp_locks_id = handler_id; + cc_wwan_device_locks_changed_cb (self); + cc_wwan_device_state_changed_cb (self); + } +} + +static void +cc_wwan_device_signal_quality_changed_cb (CcWwanDevice *self) +{ + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SIGNAL]); +} + +static void +cc_wwan_device_mode_changed_cb (CcWwanDevice *self) +{ + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NETWORK_MODE]); +} + +static void +cc_wwan_device_emit_data_changed (CcWwanDevice *self) +{ + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HAS_DATA]); +} + +static void +cc_wwan_device_unlock_required_cb (CcWwanDevice *self) +{ + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_UNLOCK_REQUIRED]); +} + +#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK) +static void +cc_wwan_device_nm_changed_cb (CcWwanDevice *self, + GParamSpec *pspec, + NMClient *client) +{ + gboolean nm_is_running; + + nm_is_running = nm_client_get_nm_running (client); + + if (!nm_is_running && self->wwan_data != NULL) + { + g_clear_object (&self->wwan_data); + cc_wwan_device_emit_data_changed (self); + } +} + +static void +cc_wwan_device_nm_device_added_cb (CcWwanDevice *self, + NMDevice *nm_device) +{ + if (!NM_IS_DEVICE_MODEM (nm_device)) + return; + + if(!self->sim || !cc_wwan_device_is_nm_device (self, G_OBJECT (nm_device))) + return; + + self->wwan_data = cc_wwan_data_new (self->mm_object, + NM_CLIENT (self->nm_client)); + + if (self->wwan_data) + { + g_signal_connect_object (self->wwan_data, "notify::enabled", + G_CALLBACK (cc_wwan_device_emit_data_changed), + self, G_CONNECT_SWAPPED); + cc_wwan_device_emit_data_changed (self); + } +} +#endif + +static void +cc_wwan_device_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcWwanDevice *self = (CcWwanDevice *)object; + MMModemMode allowed, preferred; + + switch (prop_id) + { + case PROP_OPERATOR_NAME: + g_value_set_string (value, cc_wwan_device_get_operator_name (self)); + break; + + case PROP_ERROR: + g_value_set_boolean (value, self->error != NULL); + break; + + case PROP_HAS_DATA: + g_value_set_boolean (value, self->wwan_data != NULL); + break; + + case PROP_ENABLED_LOCKS: + g_value_set_int (value, self->locks); + break; + + case PROP_NETWORK_MODE: + if (cc_wwan_device_get_current_mode (self, &allowed, &preferred)) + g_value_take_string (value, cc_wwan_device_get_string_from_mode (self, allowed, preferred)); + break; + + case PROP_REGISTRATION_STATE: + g_value_set_int (value, self->registration_state); + break; + + case PROP_UNLOCK_REQUIRED: + g_value_set_int (value, cc_wwan_device_get_lock (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_wwan_device_dispose (GObject *object) +{ + CcWwanDevice *self = (CcWwanDevice *)object; + + g_clear_error (&self->error); + g_clear_object (&self->modem); + g_clear_object (&self->mm_object); + g_clear_object (&self->sim); + g_clear_object (&self->modem_3gpp); + + g_clear_object (&self->nm_client); + g_clear_object (&self->wwan_data); + + G_OBJECT_CLASS (cc_wwan_device_parent_class)->dispose (object); +} + +static void +cc_wwan_device_class_init (CcWwanDeviceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = cc_wwan_device_get_property; + object_class->dispose = cc_wwan_device_dispose; + + properties[PROP_OPERATOR_NAME] = + g_param_spec_string ("operator-name", + "Operator Name", + "Operator Name the device is connected to", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + properties[PROP_ENABLED_LOCKS] = + g_param_spec_int ("enabled-locks", + "Enabled Locks", + "Locks Enabled in Modem", + MM_MODEM_3GPP_FACILITY_NONE, + MM_MODEM_3GPP_FACILITY_CORP_PERS, + MM_MODEM_3GPP_FACILITY_NONE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + properties[PROP_ERROR] = + g_param_spec_boolean ("error", + "Error", + "Set if some Error occurs", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + properties[PROP_HAS_DATA] = + g_param_spec_boolean ("has-data", + "has-data", + "Data for the device", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + properties[PROP_NETWORK_MODE] = + g_param_spec_string ("network-mode", + "Network Mode", + "A String representing preferred network mode", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + properties[PROP_REGISTRATION_STATE] = + g_param_spec_int ("registration-state", + "Registration State", + "The current network registration state", + CC_WWAN_REGISTRATION_STATE_UNKNOWN, + CC_WWAN_REGISTRATION_STATE_DENIED, + CC_WWAN_REGISTRATION_STATE_UNKNOWN, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + properties[PROP_UNLOCK_REQUIRED] = + g_param_spec_int ("unlock-required", + "Unlock Required", + "The Modem lock status changed", + MM_MODEM_LOCK_UNKNOWN, + MM_MODEM_LOCK_PH_NETSUB_PUK, + MM_MODEM_LOCK_UNKNOWN, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + properties[PROP_SIGNAL] = + g_param_spec_int ("signal", + "Signal", + "Get Device Signal", + 0, 100, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +cc_wwan_device_init (CcWwanDevice *self) +{ +} + +/** + * cc_wwan_device_new: + * @mm_object: (transfer full): An #MMObject + * + * Create a new device representing the given + * @mm_object. + * + * Returns: A #CcWwanDevice + */ +CcWwanDevice * +cc_wwan_device_new (MMObject *mm_object, + GObject *nm_client) +{ + CcWwanDevice *self; + + g_return_val_if_fail (MM_IS_OBJECT (mm_object), NULL); +#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK) + g_return_val_if_fail (NM_IS_CLIENT (nm_client), NULL); +#else + g_return_val_if_fail (!nm_client, NULL); +#endif + + self = g_object_new (CC_TYPE_WWAN_DEVICE, NULL); + + self->mm_object = g_object_ref (mm_object); + self->modem = mm_object_get_modem (mm_object); + self->sim = mm_modem_get_sim_sync (self->modem, NULL, NULL); + g_set_object (&self->nm_client, nm_client); + if (self->sim) + { + self->operator_code = mm_sim_get_operator_identifier (self->sim); +#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK) + self->wwan_data = cc_wwan_data_new (mm_object, + NM_CLIENT (self->nm_client)); +#endif + } + + g_signal_connect_object (self->mm_object, "notify::unlock-required", + G_CALLBACK (cc_wwan_device_unlock_required_cb), + self, G_CONNECT_SWAPPED); + if (self->wwan_data) + g_signal_connect_object (self->wwan_data, "notify::enabled", + G_CALLBACK (cc_wwan_device_emit_data_changed), + self, G_CONNECT_SWAPPED); + +#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK) + g_signal_connect_object (self->nm_client, "notify::nm-running" , + G_CALLBACK (cc_wwan_device_nm_changed_cb), self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (self->nm_client, "device-added", + G_CALLBACK (cc_wwan_device_nm_device_added_cb), + self, G_CONNECT_SWAPPED); +#endif + + g_signal_connect_object (self->mm_object, "notify::modem3gpp", + G_CALLBACK (cc_wwan_device_3gpp_changed_cb), + self, G_CONNECT_SWAPPED); + g_signal_connect_object (self->modem, "notify::signal-quality", + G_CALLBACK (cc_wwan_device_signal_quality_changed_cb), + self, G_CONNECT_SWAPPED); + + cc_wwan_device_3gpp_changed_cb (self); + g_signal_connect_object (self->modem, "notify::current-modes", + G_CALLBACK (cc_wwan_device_mode_changed_cb), + self, G_CONNECT_SWAPPED); + + return self; +} + +gboolean +cc_wwan_device_has_sim (CcWwanDevice *self) +{ + MMModemStateFailedReason state_reason; + + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + + state_reason = mm_modem_get_state_failed_reason (self->modem); + + if (state_reason == MM_MODEM_STATE_FAILED_REASON_SIM_MISSING) + return FALSE; + + return TRUE; +} + +/** + * cc_wwan_device_get_lock: + * @self: a #CcWwanDevice + * + * Get the active device lock that is required to + * be unlocked for accessing device features. + * + * Returns: %TRUE if PIN enabled, %FALSE otherwise. + */ +MMModemLock +cc_wwan_device_get_lock (CcWwanDevice *self) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), MM_MODEM_LOCK_UNKNOWN); + + return mm_modem_get_unlock_required (self->modem); +} + + +/** + * cc_wwan_device_get_sim_lock: + * @self: a #CcWwanDevice + * + * Get if SIM lock with PIN is enabled. SIM PIN + * enabled doesn’t mean that SIM is locked. + * See cc_wwan_device_get_lock(). + * + * Returns: %TRUE if PIN enabled, %FALSE otherwise. + */ +gboolean +cc_wwan_device_get_sim_lock (CcWwanDevice *self) +{ + gboolean sim_lock; + + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + + sim_lock = self->locks & MM_MODEM_3GPP_FACILITY_SIM; + + return !!sim_lock; +} + +guint +cc_wwan_device_get_unlock_retries (CcWwanDevice *self, + MMModemLock lock) +{ + MMUnlockRetries *retries; + + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), 0); + + retries = mm_modem_peek_unlock_retries (self->modem); + + return mm_unlock_retries_get (retries, lock); +} + +static void +cc_wwan_device_pin_sent_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CcWwanDevice *self; + MMSim *sim = (MMSim *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + + if (!mm_sim_send_pin_finish (sim, result, &error)) + { + self = g_task_get_source_object (G_TASK (task)); + + g_clear_error (&self->error); + self->error = g_error_copy (error); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]); + + g_task_return_error (task, g_steal_pointer (&error)); + } + else + g_task_return_boolean (task, TRUE); +} + +void +cc_wwan_device_send_pin (CcWwanDevice *self, + const gchar *pin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CC_IS_WWAN_DEVICE (self)); + g_return_if_fail (MM_IS_SIM (self->sim)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (pin && *pin); + + task = g_task_new (self, cancellable, callback, user_data); + + mm_sim_send_pin (self->sim, pin, cancellable, + cc_wwan_device_pin_sent_cb, + g_steal_pointer (&task)); +} + +gboolean +cc_wwan_device_send_pin_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +cc_wwan_device_puk_sent_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CcWwanDevice *self; + MMSim *sim = (MMSim *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + + if (!mm_sim_send_puk_finish (sim, result, &error)) + { + self = g_task_get_source_object (G_TASK (task)); + + g_clear_error (&self->error); + self->error = g_error_copy (error); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]); + + g_task_return_error (task, g_steal_pointer (&error)); + } + else + g_task_return_boolean (task, TRUE); +} + +void +cc_wwan_device_send_puk (CcWwanDevice *self, + const gchar *puk, + const gchar *pin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CC_IS_WWAN_DEVICE (self)); + g_return_if_fail (MM_IS_SIM (self->sim)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (puk && *puk); + g_return_if_fail (pin && *pin); + + task = g_task_new (self, cancellable, callback, user_data); + + mm_sim_send_puk (self->sim, puk, pin, cancellable, + cc_wwan_device_puk_sent_cb, + g_steal_pointer (&task)); +} + +gboolean +cc_wwan_device_send_puk_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +cc_wwan_device_enable_pin_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CcWwanDevice *self; + MMSim *sim = (MMSim *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + + if (!mm_sim_enable_pin_finish (sim, result, &error)) + { + self = g_task_get_source_object (G_TASK (task)); + + g_clear_error (&self->error); + self->error = g_error_copy (error); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]); + + g_task_return_error (task, g_steal_pointer (&error)); + } + else + g_task_return_boolean (task, TRUE); +} + +void +cc_wwan_device_enable_pin (CcWwanDevice *self, + const gchar *pin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CC_IS_WWAN_DEVICE (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (pin && *pin); + + task = g_task_new (self, cancellable, callback, user_data); + + mm_sim_enable_pin (self->sim, pin, cancellable, + cc_wwan_device_enable_pin_cb, + g_steal_pointer (&task)); +} + +gboolean +cc_wwan_device_enable_pin_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +cc_wwan_device_disable_pin_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CcWwanDevice *self; + MMSim *sim = (MMSim *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + + if (!mm_sim_disable_pin_finish (sim, result, &error)) + { + self = g_task_get_source_object (G_TASK (task)); + + g_clear_error (&self->error); + self->error = g_error_copy (error); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]); + + g_task_return_error (task, g_steal_pointer (&error)); + } + else + g_task_return_boolean (task, TRUE); +} + +void +cc_wwan_device_disable_pin (CcWwanDevice *self, + const gchar *pin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CC_IS_WWAN_DEVICE (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (pin && *pin); + + task = g_task_new (self, cancellable, callback, user_data); + + mm_sim_disable_pin (self->sim, pin, cancellable, + cc_wwan_device_disable_pin_cb, + g_steal_pointer (&task)); +} + +gboolean +cc_wwan_device_disable_pin_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +cc_wwan_device_change_pin_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CcWwanDevice *self; + MMSim *sim = (MMSim *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + + if (!mm_sim_change_pin_finish (sim, result, &error)) + { + self = g_task_get_source_object (G_TASK (task)); + + g_clear_error (&self->error); + self->error = g_error_copy (error); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]); + + g_task_return_error (task, g_steal_pointer (&error)); + } + else + g_task_return_boolean (task, TRUE); +} + +void +cc_wwan_device_change_pin (CcWwanDevice *self, + const gchar *old_pin, + const gchar *new_pin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CC_IS_WWAN_DEVICE (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + g_return_if_fail (old_pin && *old_pin); + g_return_if_fail (new_pin && *new_pin); + + task = g_task_new (self, cancellable, callback, user_data); + + mm_sim_change_pin (self->sim, old_pin, new_pin, cancellable, + cc_wwan_device_change_pin_cb, + g_steal_pointer (&task)); +} + +gboolean +cc_wwan_device_change_pin_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +cc_wwan_device_network_mode_set_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CcWwanDevice *self; + MMModem *modem = (MMModem *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + + if (!mm_modem_set_current_modes_finish (modem, result, &error)) + { + self = g_task_get_source_object (G_TASK (task)); + + g_clear_error (&self->error); + self->error = g_error_copy (error); + g_warning ("Error: %s", error->message); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]); + + g_task_return_error (task, g_steal_pointer (&error)); + } + else + g_task_return_boolean (task, TRUE); +} + +/** + * cc_wwan_device_set_network_mode: + * @self: a #CcWwanDevice + * @allowed: The allowed #MMModemModes + * @preferred: The preferred #MMModemMode + * @cancellable: (nullable): a #GCancellable or %NULL + * @callback: (nullable): a #GAsyncReadyCallback or %NULL + * @user_data: (nullable): closure data for @callback + * + * Asynchronously set preferred network mode. + * + * Call @cc_wwan_device_set_current_mode_finish() + * in @callback to get the result of operation. + */ +void +cc_wwan_device_set_current_mode (CcWwanDevice *self, + MMModemMode allowed, + MMModemMode preferred, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + GPermission *permission; + g_autoptr(GError) error = NULL; + + g_return_if_fail (CC_IS_WWAN_DEVICE (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + permission = polkit_permission_new_sync ("org.freedesktop.ModemManager1.Device.Control", + NULL, cancellable, &error); + g_task_set_task_data (task, permission, g_object_unref); + + if (error) + g_warning ("error: %s", error->message); + + if (error) + g_task_return_error (task, g_steal_pointer (&error)); + else if (!g_permission_get_allowed (permission)) + { + error = g_error_new (G_IO_ERROR, + G_IO_ERROR_PERMISSION_DENIED, + "Access Denied"); + g_clear_error (&self->error); + self->error = g_error_copy (error); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]); + + g_task_return_error (task, g_steal_pointer (&error)); + } + else + mm_modem_set_current_modes (self->modem, allowed, preferred, + cancellable, cc_wwan_device_network_mode_set_cb, + g_steal_pointer (&task)); +} + +/** + * cc_wwan_device_set_current_mode_finish: + * @self: a #CcWwanDevice + * @result: a #GAsyncResult + * @error: a location for #GError or %NULL + * + * Get the status whether setting network mode + * succeeded + * + * Returns: %TRUE if network mode was successfully set, + * %FALSE otherwise. + */ +gboolean +cc_wwan_device_set_current_mode_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +cc_wwan_device_get_current_mode (CcWwanDevice *self, + MMModemMode *allowed, + MMModemMode *preferred) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + + return mm_modem_get_current_modes (self->modem, allowed, preferred); +} + +gboolean +cc_wwan_device_is_auto_network (CcWwanDevice *self) +{ + /* + * XXX: ModemManager Doesn’t have a true API to check + * if registration is automatic or manual. So Let’s + * do some guess work. + */ + if (self->registration_state == CC_WWAN_REGISTRATION_STATE_DENIED) + return FALSE; + + return !self->network_is_manual; +} + +CcWwanState +cc_wwan_device_get_network_state (CcWwanDevice *self) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), 0); + + return self->registration_state; +} + +gboolean +cc_wwan_device_get_supported_modes (CcWwanDevice *self, + MMModemMode *allowed, + MMModemMode *preferred) +{ + g_autofree MMModemModeCombination *modes = NULL; + guint n_modes, i; + + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + + if (!mm_modem_get_supported_modes (self->modem, &modes, &n_modes)) + return FALSE; + + if (allowed) + *allowed = 0; + if (preferred) + *preferred = 0; + + for (i = 0; i < n_modes; i++) + { + if (allowed) + *allowed = *allowed | modes[i].allowed; + if (preferred) + *preferred = *preferred | modes[i].preferred; + } + + return TRUE; +} + +#define APPEND_MODE_TO_STRING(_str, _now, _preferred, _mode_str) do { \ + if (_str->len > 0) \ + g_string_append (_str, ", "); \ + g_string_append (_str, _mode_str); \ + if (_preferred == _now) \ + g_string_append (_str, _(" (Preferred)")); \ + } while (0) + +gchar * +cc_wwan_device_get_string_from_mode (CcWwanDevice *self, + MMModemMode allowed, + MMModemMode preferred) +{ + GString *str; + + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL); + g_return_val_if_fail (allowed != 0, NULL); + + if (allowed == MM_MODEM_MODE_2G) + return g_strdup (_("2G Only")); + if (allowed == MM_MODEM_MODE_3G) + return g_strdup (_("3G Only")); + if (allowed == MM_MODEM_MODE_4G) + return g_strdup (_("4G Only")); + + str = g_string_sized_new (10); + + if (allowed & MM_MODEM_MODE_2G) + APPEND_MODE_TO_STRING (str, MM_MODEM_MODE_2G, preferred, "2G"); + if (allowed & MM_MODEM_MODE_3G) + APPEND_MODE_TO_STRING (str, MM_MODEM_MODE_3G, preferred, "3G"); + if (allowed & MM_MODEM_MODE_4G) + APPEND_MODE_TO_STRING (str, MM_MODEM_MODE_4G, preferred, "4G"); + + if (str->len == 0) + return g_string_free (str, TRUE); + else + return g_string_free (str, FALSE); +} +#undef APPEND_MODE_TO_STRING + +static void +wwan_network_list_free (GList *network_list) +{ + g_list_free_full (network_list, (GDestroyNotify)mm_modem_3gpp_network_free); +} + +static void +cc_wwan_device_scan_complete_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + MMModem3gpp *modem_3gpp = (MMModem3gpp *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + GList *network_list; + + network_list = mm_modem_3gpp_scan_finish (modem_3gpp, result, &error); + + if (error) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_pointer (task, network_list, (GDestroyNotify)wwan_network_list_free); +} + +void +cc_wwan_device_scan_networks (CcWwanDevice *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CC_IS_WWAN_DEVICE (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + + mm_modem_3gpp_scan (self->modem_3gpp, cancellable, + cc_wwan_device_scan_complete_cb, + g_steal_pointer (&task)); +} + +GList * +cc_wwan_device_scan_networks_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +cc_wwan_device_register_network_complete_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + CcWwanDevice *self; + MMModem3gpp *modem_3gpp = (MMModem3gpp *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + + if (!mm_modem_3gpp_register_finish (modem_3gpp, result, &error)) + { + self = g_task_get_source_object (G_TASK (task)); + + g_clear_error (&self->error); + self->error = g_error_copy (error); + g_warning ("Error: %s", error->message); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]); + + g_task_return_error (task, g_steal_pointer (&error)); + } + else + g_task_return_boolean (task, TRUE); +} + +void +cc_wwan_device_register_network (CcWwanDevice *self, + const gchar *network_id, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CC_IS_WWAN_DEVICE (self)); + g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + + if (network_id && *network_id) + self->network_is_manual = TRUE; + else + self->network_is_manual = FALSE; + + mm_modem_3gpp_register (self->modem_3gpp, network_id, cancellable, + cc_wwan_device_register_network_complete_cb, + g_steal_pointer (&task)); +} + +gboolean +cc_wwan_device_register_network_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +/** + * cc_wwan_device_get_operator_name: + * @self: a #CcWwanDevice + * + * Get the human readable network operator name + * currently the device is connected to. + * + * Returns: (nullable): The operator name or %NULL + */ +const gchar * +cc_wwan_device_get_operator_name (CcWwanDevice *self) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL); + + if (!self->modem_3gpp) + return NULL; + + return mm_modem_3gpp_get_operator_name (self->modem_3gpp); +} + +gchar * +cc_wwan_device_dup_sim_identifier (CcWwanDevice *self) +{ + char *identifier; + + identifier = mm_sim_dup_operator_name (self->sim); + if (identifier) + return identifier; + + identifier = mm_sim_dup_operator_identifier (self->sim); + if (identifier) + return identifier; + + identifier = mm_sim_dup_identifier (self->sim); + if (identifier) + return identifier; + + return g_strdup (""); +} + +gchar * +cc_wwan_device_dup_network_type_string (CcWwanDevice *self) +{ + MMModemAccessTechnology type; + + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL); + + type = mm_modem_get_access_technologies (self->modem); + + return mm_modem_access_technology_build_string_from_mask (type); +} + +gchar * +cc_wwan_device_dup_signal_string (CcWwanDevice *self) +{ + MMModemSignal *modem_signal; + MMSignal *signal; + GString *str; + gdouble value; + gboolean recent; + + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL); + + modem_signal = mm_object_peek_modem_signal (self->mm_object); + + if (!modem_signal) + return g_strdup_printf ("%d%%", mm_modem_get_signal_quality (self->modem, &recent)); + + str = g_string_new (""); + + /* Adapted from ModemManager mmcli-modem-signal.c */ + signal = mm_modem_signal_peek_cdma (modem_signal); + if (signal) + { + if ((value = mm_signal_get_rssi (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "rssi: %.2g dBm ", value); + if ((value = mm_signal_get_ecio (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "ecio: %.2g dBm ", value); + } + + signal = mm_modem_signal_peek_evdo (modem_signal); + if (signal) + { + if ((value = mm_signal_get_rssi (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "rssi: %.2g dBm ", value); + if ((value = mm_signal_get_ecio (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "ecio: %.2g dBm ", value); + if ((value = mm_signal_get_sinr (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "sinr: %.2g dB ", value); + if ((value = mm_signal_get_io (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "io: %.2g dBm ", value); + } + + signal = mm_modem_signal_peek_gsm (modem_signal); + if (signal) + if ((value = mm_signal_get_rssi (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "rssi: %.2g dBm ", value); + + signal = mm_modem_signal_peek_umts (modem_signal); + if (signal) + { + if ((value = mm_signal_get_rssi (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "rssi: %.2g dBm ", value); + if ((value = mm_signal_get_rscp (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "rscp: %.2g dBm ", value); + if ((value = mm_signal_get_ecio (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "ecio: %.2g dBm ", value); + } + + signal = mm_modem_signal_peek_lte (modem_signal); + if (signal) + { + if ((value = mm_signal_get_rssi (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "rssi: %.2g dBm ", value); + if ((value = mm_signal_get_rsrq (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "rsrq: %.2g dB ", value); + if ((value = mm_signal_get_rsrp (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "rsrp: %.2g dBm ", value); + if ((value = mm_signal_get_snr (signal)) != MM_SIGNAL_UNKNOWN) + g_string_append_printf (str, "snr: %.2g dB ", value); + } + + return g_string_free (str, FALSE); +} + +const gchar * +cc_wwan_device_get_manufacturer (CcWwanDevice *self) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL); + + return mm_modem_get_manufacturer (self->modem); +} + +const gchar * +cc_wwan_device_get_model (CcWwanDevice *self) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL); + + return mm_modem_get_model (self->modem); +} + +const gchar * +cc_wwan_device_get_firmware_version (CcWwanDevice *self) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL); + + return mm_modem_get_revision (self->modem); +} + +const gchar * +cc_wwan_device_get_identifier (CcWwanDevice *self) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL); + + return mm_modem_get_equipment_identifier (self->modem); +} + +const gchar * +cc_wwan_device_get_simple_error (CcWwanDevice *self) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL); + + if (!self->error) + return NULL; + + return cc_wwan_error_get_message (self->error); +} + +gboolean +cc_wwan_device_is_nm_device (CcWwanDevice *self, + GObject *nm_device) +{ +#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK) + g_return_val_if_fail (NM_IS_DEVICE (nm_device), FALSE); + + return g_str_equal (mm_modem_get_primary_port (self->modem), + nm_device_get_iface (NM_DEVICE (nm_device))); +#else + return FALSE; +#endif +} + +const gchar * +cc_wwan_device_get_path (CcWwanDevice *self) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), ""); + + return mm_object_get_path (self->mm_object); +} + +CcWwanData * +cc_wwan_device_get_data (CcWwanDevice *self) +{ + g_return_val_if_fail (CC_IS_WWAN_DEVICE (self), NULL); + + return self->wwan_data; +} + +gboolean +cc_wwan_device_pin_valid (const gchar *password, + MMModemLock lock) +{ + size_t len; + + g_return_val_if_fail (lock == MM_MODEM_LOCK_SIM_PIN || + lock == MM_MODEM_LOCK_SIM_PIN2 || + lock == MM_MODEM_LOCK_SIM_PUK || + lock == MM_MODEM_LOCK_SIM_PUK2, FALSE); + if (!password) + return FALSE; + + len = strlen (password); + + if (len < 4 || len > 8) + return FALSE; + + if (strspn (password, "0123456789") != len) + return FALSE; + + /* + * XXX: Can PUK code be something other than 8 digits? + * 3GPP standard seems mum on this + */ + if (lock == MM_MODEM_LOCK_SIM_PUK || + lock == MM_MODEM_LOCK_SIM_PUK2) + if (len != 8) + return FALSE; + + return TRUE; +} diff --git a/plugins/wwan/cc-wwan-device.h b/plugins/wwan/cc-wwan-device.h new file mode 100644 index 0000000..add27d3 --- /dev/null +++ b/plugins/wwan/cc-wwan-device.h @@ -0,0 +1,152 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* cc-wwan-device.h + * + * Copyright 2019-2020 Purism SPC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <glib-object.h> +#include <libmm-glib.h> + +#if defined(HAVE_NETWORK_MANAGER) && defined(BUILD_NETWORK) +# include "cc-wwan-data.h" +#endif + +G_BEGIN_DECLS + +typedef enum +{ + CC_WWAN_REGISTRATION_STATE_UNKNOWN, + CC_WWAN_REGISTRATION_STATE_IDLE, + CC_WWAN_REGISTRATION_STATE_REGISTERED, + CC_WWAN_REGISTRATION_STATE_ROAMING, + CC_WWAN_REGISTRATION_STATE_SEARCHING, + CC_WWAN_REGISTRATION_STATE_DENIED +} CcWwanState; + +typedef struct _CcWwanData CcWwanData; + +#define CC_TYPE_WWAN_DEVICE (cc_wwan_device_get_type()) +G_DECLARE_FINAL_TYPE (CcWwanDevice, cc_wwan_device, CC, WWAN_DEVICE, GObject) + +CcWwanDevice *cc_wwan_device_new (MMObject *mm_object, + GObject *nm_client); +gboolean cc_wwan_device_has_sim (CcWwanDevice *self); +MMModemLock cc_wwan_device_get_lock (CcWwanDevice *self); +gboolean cc_wwan_device_get_sim_lock (CcWwanDevice *self); +guint cc_wwan_device_get_unlock_retries (CcWwanDevice *self, + MMModemLock lock); +void cc_wwan_device_enable_pin (CcWwanDevice *self, + const gchar *pin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cc_wwan_device_enable_pin_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error); +void cc_wwan_device_disable_pin (CcWwanDevice *self, + const gchar *pin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cc_wwan_device_disable_pin_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error); +void cc_wwan_device_send_pin (CcWwanDevice *self, + const gchar *pin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cc_wwan_device_send_pin_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error); +void cc_wwan_device_send_puk (CcWwanDevice *self, + const gchar *puk, + const gchar *pin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cc_wwan_device_send_puk_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error); +void cc_wwan_device_change_pin (CcWwanDevice *self, + const gchar *old_pin, + const gchar *new_pin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cc_wwan_device_change_pin_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error); +const gchar *cc_wwan_device_get_operator_name (CcWwanDevice *self); +gchar *cc_wwan_device_dup_sim_identifier (CcWwanDevice *self); +gchar *cc_wwan_device_dup_network_type_string (CcWwanDevice *self); +gchar *cc_wwan_device_dup_signal_string (CcWwanDevice *self); +const gchar *cc_wwan_device_get_manufacturer (CcWwanDevice *self); +const gchar *cc_wwan_device_get_model (CcWwanDevice *self); +const gchar *cc_wwan_device_get_firmware_version (CcWwanDevice *self); +const gchar *cc_wwan_device_get_identifier (CcWwanDevice *self); +gboolean cc_wwan_device_get_current_mode (CcWwanDevice *self, + MMModemMode *allowed, + MMModemMode *preferred); +gboolean cc_wwan_device_is_auto_network (CcWwanDevice *self); +CcWwanState cc_wwan_device_get_network_state (CcWwanDevice *self); +gboolean cc_wwan_device_get_supported_modes (CcWwanDevice *self, + MMModemMode *allowed, + MMModemMode *preferred); +void cc_wwan_device_set_current_mode (CcWwanDevice *self, + MMModemMode allowed, + MMModemMode preferred, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cc_wwan_device_set_current_mode_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error); +gchar *cc_wwan_device_get_string_from_mode (CcWwanDevice *self, + MMModemMode allowed, + MMModemMode preferred); +void cc_wwan_device_scan_networks (CcWwanDevice *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GList *cc_wwan_device_scan_networks_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error); +void cc_wwan_device_register_network (CcWwanDevice *self, + const gchar *network_id, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean cc_wwan_device_register_network_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error); +const gchar *cc_wwan_device_get_simple_error (CcWwanDevice *self); +GSList *cc_wwan_device_get_apn_list (CcWwanDevice *self); +gboolean cc_wwan_device_is_nm_device (CcWwanDevice *self, + GObject *nm_device); +const gchar *cc_wwan_device_get_path (CcWwanDevice *self); +CcWwanData *cc_wwan_device_get_data (CcWwanDevice *self); +gboolean cc_wwan_device_pin_valid (const gchar *password, + MMModemLock lock); + +G_END_DECLS diff --git a/plugins/wwan/cc-wwan-errors-private.h b/plugins/wwan/cc-wwan-errors-private.h new file mode 100644 index 0000000..955d6ee --- /dev/null +++ b/plugins/wwan/cc-wwan-errors-private.h @@ -0,0 +1,104 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* cc-wwan-errors-private.h + * + * Copyright 2019 Purism SPC + * + * Modified from mm-error-helpers.c from ModemManager + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Author(s): + * Mohammed Sadiq <sadiq@sadiqpk.org> + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <glib/gi18n.h> +#include <glib-object.h> +#include <libmm-glib.h> + +typedef struct { + guint code; + const gchar *message; +} ErrorTable; + + +static ErrorTable me_errors[] = { + { MM_MOBILE_EQUIPMENT_ERROR_PHONE_FAILURE, N_("Phone failure") }, + { MM_MOBILE_EQUIPMENT_ERROR_NO_CONNECTION, N_("No connection to phone") }, + { MM_MOBILE_EQUIPMENT_ERROR_LINK_RESERVED, "Phone-adaptor link reserved" }, + { MM_MOBILE_EQUIPMENT_ERROR_NOT_ALLOWED, N_("Operation not allowed") }, + { MM_MOBILE_EQUIPMENT_ERROR_NOT_SUPPORTED, N_("Operation not supported") }, + { MM_MOBILE_EQUIPMENT_ERROR_PH_SIM_PIN, "PH-SIM PIN required" }, + { MM_MOBILE_EQUIPMENT_ERROR_PH_FSIM_PIN, "PH-FSIM PIN required" }, + { MM_MOBILE_EQUIPMENT_ERROR_PH_FSIM_PUK, "PH-FSIM PUK required" }, + { MM_MOBILE_EQUIPMENT_ERROR_SIM_NOT_INSERTED, N_("SIM not inserted") }, + { MM_MOBILE_EQUIPMENT_ERROR_SIM_PIN, N_("SIM PIN required") }, + { MM_MOBILE_EQUIPMENT_ERROR_SIM_PUK, N_("SIM PUK required") }, + { MM_MOBILE_EQUIPMENT_ERROR_SIM_FAILURE, N_("SIM failure") }, + { MM_MOBILE_EQUIPMENT_ERROR_SIM_BUSY, N_("SIM busy") }, + { MM_MOBILE_EQUIPMENT_ERROR_SIM_WRONG, N_("SIM wrong") }, + { MM_MOBILE_EQUIPMENT_ERROR_INCORRECT_PASSWORD, N_("Incorrect password") }, + { MM_MOBILE_EQUIPMENT_ERROR_SIM_PIN2, N_("SIM PIN2 required") }, + { MM_MOBILE_EQUIPMENT_ERROR_SIM_PUK2, N_("SIM PUK2 required") }, + { MM_MOBILE_EQUIPMENT_ERROR_MEMORY_FULL, "Memory full" }, + { MM_MOBILE_EQUIPMENT_ERROR_INVALID_INDEX, "Invalid index" }, + { MM_MOBILE_EQUIPMENT_ERROR_NOT_FOUND, "Not found" }, + { MM_MOBILE_EQUIPMENT_ERROR_MEMORY_FAILURE, "Memory failure" }, + { MM_MOBILE_EQUIPMENT_ERROR_NO_NETWORK, "No network service" }, + { MM_MOBILE_EQUIPMENT_ERROR_NETWORK_TIMEOUT, "Network timeout" }, + { MM_MOBILE_EQUIPMENT_ERROR_NETWORK_NOT_ALLOWED, "Network not allowed - emergency calls only" }, + { MM_MOBILE_EQUIPMENT_ERROR_NETWORK_PIN, "Network personalization PIN required" }, + { MM_MOBILE_EQUIPMENT_ERROR_NETWORK_PUK, "Network personalization PUK required" }, + { MM_MOBILE_EQUIPMENT_ERROR_NETWORK_SUBSET_PIN, "Network subset personalization PIN required" }, + { MM_MOBILE_EQUIPMENT_ERROR_NETWORK_SUBSET_PUK, "Network subset personalization PUK required" }, + { MM_MOBILE_EQUIPMENT_ERROR_SERVICE_PIN, "Service provider personalization PIN required" }, + { MM_MOBILE_EQUIPMENT_ERROR_SERVICE_PUK, "Service provider personalization PUK required" }, + { MM_MOBILE_EQUIPMENT_ERROR_CORP_PIN, "Corporate personalization PIN required" }, + { MM_MOBILE_EQUIPMENT_ERROR_CORP_PUK, "Corporate personalization PUK required" }, + { MM_MOBILE_EQUIPMENT_ERROR_UNKNOWN, N_("Unknown error") }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_ILLEGAL_MS, "Illegal MS" }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_ILLEGAL_ME, "Illegal ME" }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_SERVICE_NOT_ALLOWED, "GPRS services not allowed" }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_PLMN_NOT_ALLOWED, "PLMN not allowed" }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_LOCATION_NOT_ALLOWED, "Location area not allowed" }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_ROAMING_NOT_ALLOWED, "Roaming not allowed in this location area" }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_SERVICE_OPTION_NOT_SUPPORTED, "Service option not supported" }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_SERVICE_OPTION_NOT_SUBSCRIBED, "Requested service option not subscribed" }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_SERVICE_OPTION_OUT_OF_ORDER, "Service option temporarily out of order" }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_UNKNOWN, "Unspecified GPRS error" }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_PDP_AUTH_FAILURE, "PDP authentication failure" }, + { MM_MOBILE_EQUIPMENT_ERROR_GPRS_INVALID_MOBILE_CLASS, "Invalid mobile class" }, +}; + +static inline const gchar * +cc_wwan_error_get_message (GError *error) +{ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return _("Action Cancelled"); + + if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED)) + return _("Access denied"); + + if (error->domain != MM_MOBILE_EQUIPMENT_ERROR) + return error->message; + + for (guint i = 0; i < G_N_ELEMENTS (me_errors); i++) + if (me_errors[i].code == error->code) + return _(me_errors[i].message); + + return _("Unknown Error"); +} diff --git a/plugins/wwan/gsd-wwan-manager.c b/plugins/wwan/gsd-wwan-manager.c new file mode 100644 index 0000000..42109ef --- /dev/null +++ b/plugins/wwan/gsd-wwan-manager.c @@ -0,0 +1,826 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2019 Purism SPC + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Author: Guido Günther <agx@sigxcpu.org> + * + */ + +#include "config.h" + +#include <string.h> +#include <locale.h> + +#include <gio/gio.h> +#include <glib.h> +#include <glib/gi18n.h> + +#include <libmm-glib.h> + +#define GCR_API_SUBJECT_TO_CHANGE +#include <gcr/gcr-base.h> + +#include "gnome-settings-profile.h" +#include "cc-wwan-device.h" +#include "cc-wwan-errors-private.h" +#include "gsd-wwan-manager.h" + + +struct _GsdWwanManager +{ + GObject parent; + + guint start_idle_id; + gboolean unlock; + GSettings *settings; + + /* List of all devices not in ‘devices_to_unlock’ */ + GPtrArray *devices; + GPtrArray *devices_to_unlock; + + /* Currently shown prompt and device being unlocked */ + GcrPrompt *prompt; + CcWwanDevice *unlocking_device; + GCancellable *cancellable; + char *puk_code; /* Used only for PUK unlock */ + guint prompt_timeout_id; + + MMManager *mm1; + gboolean mm1_running; +}; + +enum { + PROP_0, + PROP_UNLOCK_SIM, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +#define GSD_WWAN_SCHEMA_DIR "org.gnome.settings-daemon.plugins.wwan" +#define GSD_WWAN_SCHEMA_UNLOCK_SIM "unlock-sim" + +G_DEFINE_TYPE (GsdWwanManager, gsd_wwan_manager, G_TYPE_OBJECT) + +/* The plugin's manager object */ +static gpointer manager_object = NULL; + +static void wwan_manager_ensure_unlocking (GsdWwanManager *self); +static void wwan_manager_unlock_device (CcWwanDevice *device, + gpointer user_data); +static void wwan_manager_unlock_required_cb (GsdWwanManager *self, + GParamSpec *pspec, + CcWwanDevice *device); + +static void +manager_unlock_prompt_new (GsdWwanManager *self, + CcWwanDevice *device, + MMModemLock lock, + const char *msg, + gboolean new_password) +{ + g_autoptr(GError) error = NULL; + g_autofree gchar *identifier = NULL; + g_autofree gchar *description = NULL; + g_autofree gchar *warning = NULL; + const gchar *message = NULL; + guint retries; + + identifier = cc_wwan_device_dup_sim_identifier (device); + g_debug ("Creating new PIN/PUK dialog for SIM %s", identifier); + + if (!self->prompt) + self->prompt = gcr_system_prompt_open (-1, self->cancellable, &error); + + if (!self->prompt) { + if (error->code == GCR_SYSTEM_PROMPT_IN_PROGRESS) + g_warning ("Another Gcr system prompt is already in progress."); + else + g_warning ("Couldn't create prompt for SIM Code entry: %s", error->message); + return; + } + + /* Set up the dialog */ + if (new_password) { + gcr_prompt_set_title (self->prompt, _("New PIN for SIM")); + gcr_prompt_set_continue_label (self->prompt, _("Set")); + } else { + gcr_prompt_set_title (self->prompt, _("Unlock SIM card")); + gcr_prompt_set_continue_label (self->prompt, _("Unlock")); + } + + gcr_prompt_set_cancel_label (self->prompt, _("Cancel")); + gcr_prompt_set_password_new (self->prompt, new_password); + + if (lock == MM_MODEM_LOCK_SIM_PIN) { + if (new_password) { + description = g_strdup_printf (_("Please provide a new PIN for SIM card %s"), + identifier); + message = _("Enter a New PIN to unlock your SIM card"); + } else { + description = g_strdup_printf (_("Please provide the PIN for SIM card %s"), + identifier); + message = _("Enter PIN to unlock your SIM card"); + } + } else if (lock == MM_MODEM_LOCK_SIM_PUK) { + description = g_strdup_printf (_("Please provide the PUK for SIM card %s"), + identifier); + message = _("Enter PUK to unlock your SIM card"); + } else { + g_warning ("Unsupported lock type: %u", lock); + g_clear_object (&self->prompt); + return; + } + + gcr_prompt_set_description (self->prompt, description); + gcr_prompt_set_message (self->prompt, message); + + if (!new_password) + retries = cc_wwan_device_get_unlock_retries (device, lock); + + if (!new_password && retries != MM_UNLOCK_RETRIES_UNKNOWN) { + if (msg) { + /* msg is already localised */ + warning = g_strdup_printf (ngettext ("%2$s. You have %1$u try left", + "%2$s. You have %1$u tries left", retries), + retries, msg); + } else { + warning = g_strdup_printf (ngettext ("You have %u try left", + "You have %u tries left", retries), + retries); + } + } else if (msg) { + warning = g_strdup (msg); + } + + gcr_prompt_set_warning (self->prompt, warning); + + /* TODO */ + /* if (lock == MM_MODEM_LOCK_SIM_PIN) */ + /* gcr_prompt_set_choice_label (prompt, _("Automatically unlock this SIM card")); */ +} + +static gboolean +unlock_device (gpointer user_data) +{ + GsdWwanManager *self; + CcWwanDevice *device; + g_autoptr(GTask) task = user_data; + MMModemLock lock; + + g_assert (G_IS_TASK (task)); + + self = g_task_get_task_data (task); + device = g_task_get_source_object (task); + + g_assert (GSD_IS_WWAN_MANAGER (self)); + g_assert (CC_IS_WWAN_DEVICE (device)); + + self->prompt_timeout_id = 0; + + if (g_task_return_error_if_cancelled (task)) + return G_SOURCE_REMOVE; + + lock = cc_wwan_device_get_lock (device); + + if (lock != MM_MODEM_LOCK_SIM_PIN && + lock != MM_MODEM_LOCK_SIM_PUK) { + g_cancellable_cancel (g_task_get_cancellable (task)); + g_task_return_error_if_cancelled (task); + return G_SOURCE_REMOVE; + } + + wwan_manager_unlock_device (device, g_steal_pointer (&task)); + + return G_SOURCE_REMOVE; +} + +static void +wwan_manager_password_sent_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GsdWwanManager *self; + CcWwanDevice *device = (CcWwanDevice *)object; + g_autoptr(GTask) task = user_data; + g_autoptr(GError) error = NULL; + gboolean ret; + + g_assert (CC_IS_WWAN_DEVICE (device)); + g_assert (G_IS_TASK (task)); + + self = g_task_get_task_data (task); + g_assert (GSD_IS_WWAN_MANAGER (self)); + + if (self->puk_code) + ret = cc_wwan_device_send_puk_finish (device, result, &error); + else + ret = cc_wwan_device_send_pin_finish (device, result, &error); + + g_clear_pointer (&self->puk_code, gcr_secure_memory_free); + + /* Ask again if a failable error occured */ + if (error && + (g_error_matches (error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_INCORRECT_PASSWORD) || + g_error_matches (error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PUK) || + g_error_matches (error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_UNKNOWN))) { + g_object_set_data (G_OBJECT (task), "error", (gpointer)cc_wwan_error_get_message (error)); + /* ModemManager updates the lock status after some delay. Wait around 250 milliseconds + * so that the values are updated. + */ + self->prompt_timeout_id = g_timeout_add (250, unlock_device, g_steal_pointer (&task)); + + return; + } + + if (ret) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, error); + +} + +static gboolean +wwan_manager_unlock_device_finish (CcWwanDevice *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static const char * +wwan_manager_show_prompt (GsdWwanManager *self, + CcWwanDevice *device, + GTask *task) +{ + g_autoptr(GError) error = NULL; + const char *code; + + g_assert (GSD_IS_WWAN_MANAGER (self)); + g_assert (CC_IS_WWAN_DEVICE (device)); + g_assert (G_IS_TASK (task)); + + if (!self->prompt) { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Failed to create a new prompt"); + return NULL; + } + + g_set_object (&self->unlocking_device, device); + + /* Irritate user if an empty password is provided */ + do { + code = gcr_prompt_password_run (self->prompt, self->cancellable, &error); + } while (code && !*code); + + if (error) { + g_task_return_error (task, g_steal_pointer (&error)); + return NULL; + } + + /* User cancelled the dialog */ + if (!code) { + g_cancellable_cancel (g_task_get_cancellable (task)); + g_task_return_error_if_cancelled (task); + return NULL; + } + + return code; +} + +static void +wwan_manager_unlock_device (CcWwanDevice *device, + gpointer user_data) +{ + GsdWwanManager *self; + g_autoptr(GTask) task = user_data; + GCancellable *cancellable; + const char *code, *error_msg; + MMModemLock lock; + + g_assert (CC_IS_WWAN_DEVICE (device)); + g_assert (G_IS_TASK (task)); + + self = g_task_get_task_data (task); + g_assert (GSD_IS_WWAN_MANAGER (self)); + + error_msg = g_object_get_data (G_OBJECT (task), "error"); + lock = cc_wwan_device_get_lock (device); + manager_unlock_prompt_new (self, device, lock, error_msg, FALSE); + g_object_set_data (G_OBJECT (task), "error", NULL); + + code = wwan_manager_show_prompt (self, device, task); + if (!code) + return; + + if (lock == MM_MODEM_LOCK_SIM_PUK) { + gcr_secure_memory_free (self->puk_code); + self->puk_code = gcr_secure_memory_strdup (code); + + manager_unlock_prompt_new (self, device, MM_MODEM_LOCK_SIM_PIN, NULL, TRUE); + code = wwan_manager_show_prompt (self, device, task); + if (!code) + return; + } + + cancellable = g_task_get_cancellable (task); + + if (lock == MM_MODEM_LOCK_SIM_PIN) + cc_wwan_device_send_pin (device, code, cancellable, + wwan_manager_password_sent_cb, + g_steal_pointer (&task)); + else if (lock == MM_MODEM_LOCK_SIM_PUK) + cc_wwan_device_send_puk (device, self->puk_code, code, cancellable, + wwan_manager_password_sent_cb, + g_steal_pointer (&task)); +} + +static void +wwan_manager_unlock_device_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GsdWwanManager) self = user_data; + CcWwanDevice *device = (CcWwanDevice *)object; + g_autoptr(GError) error = NULL; + + g_assert (GSD_IS_WWAN_MANAGER (self)); + g_assert (CC_IS_WWAN_DEVICE (device)); + g_assert (G_IS_TASK (result)); + + wwan_manager_unlock_device_finish (device, result, &error); + + /* Move the device from devices to unlock to the list of devices */ + if (g_ptr_array_remove (self->devices_to_unlock, device)) + g_ptr_array_add (self->devices, g_object_ref (device)); + + g_clear_pointer (&self->puk_code, gcr_secure_memory_free); + g_clear_object (&self->prompt); + g_clear_object (&self->cancellable); + g_clear_object (&self->unlocking_device); + g_clear_handle_id (&self->prompt_timeout_id, g_source_remove); + + /* Unlock the next device */ + if (self->devices_to_unlock->len) + wwan_manager_unlock_required_cb (self, NULL, self->devices_to_unlock->pdata[0]); + + if (error) + g_debug ("Error unlocking device: %s", error->message); + + if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Error unlocking device: %s", error->message); +} + +static void +wwan_manager_unlock_required_cb (GsdWwanManager *self, + GParamSpec *pspec, + CcWwanDevice *device) +{ + MMModemLock lock; + + g_assert (GSD_IS_WWAN_MANAGER (self)); + g_assert (CC_IS_WWAN_DEVICE (device)); + + lock = cc_wwan_device_get_lock (device); + + if (lock != MM_MODEM_LOCK_SIM_PIN && + lock != MM_MODEM_LOCK_SIM_PUK) { + g_object_ref (device); + + /* Move the device from devices to unlock to the list of devices */ + if (g_ptr_array_remove (self->devices_to_unlock, device)) + g_ptr_array_add (self->devices, device); + + /* If the device is the device being unlocked, cancel the process */ + if (device == self->unlocking_device) + g_cancellable_cancel (self->cancellable); + } else if (lock == MM_MODEM_LOCK_SIM_PIN || + lock == MM_MODEM_LOCK_SIM_PUK) { + g_object_ref (device); + + /* Move the device to devices to unlock from the list of devices */ + if (g_ptr_array_remove (self->devices, device)) { + g_ptr_array_add (self->devices_to_unlock, device); + wwan_manager_ensure_unlocking (self); + } + } +} + + +static gboolean +device_match_by_object (CcWwanDevice *device, GDBusObject *object) +{ + const char *device_path, *object_path; + + g_return_val_if_fail (G_IS_DBUS_OBJECT (object), FALSE); + g_return_val_if_fail (CC_IS_WWAN_DEVICE (device), FALSE); + + device_path = cc_wwan_device_get_path (device); + object_path = mm_object_get_path (MM_OBJECT (object)); + + return g_strcmp0 (device_path, object_path) == 0; +} + +/* + * @array: (out) (nullable): + * @index: (out) (nullable): + * + * Returns: %TRUE if found. %FALSE otherwise + */ +static gboolean +wwan_manager_find_match (GsdWwanManager *self, + GDBusObject *object, + GPtrArray **array, + guint *index) +{ + GPtrArray *devices = NULL; + guint i = 0; + + g_return_val_if_fail (G_IS_DBUS_OBJECT (object), FALSE); + + if (g_ptr_array_find_with_equal_func (self->devices, + object, + (GEqualFunc) device_match_by_object, + &i)) + devices = self->devices; + else if (g_ptr_array_find_with_equal_func (self->devices_to_unlock, + object, + (GEqualFunc) device_match_by_object, + &i)) + devices = self->devices_to_unlock; + + if (index && i >= 0) + *index = i; + if (array) + *array = devices; + + if (devices) + return TRUE; + + return FALSE; +} + + +static void +wwan_manager_ensure_unlocking (GsdWwanManager *self) +{ + CcWwanDevice *device; + GTask *task; + + g_assert (GSD_WWAN_MANAGER (self)); + + if (!self->unlock || self->unlocking_device) + return; + + if (self->devices_to_unlock->len == 0) + return; + + g_warn_if_fail (!self->cancellable); + g_clear_object (&self->cancellable); + + device = self->devices_to_unlock->pdata[0]; + self->cancellable = g_cancellable_new (); + task = g_task_new (device, self->cancellable, + wwan_manager_unlock_device_cb, + g_object_ref (self)); + g_task_set_task_data (task, g_object_ref (self), g_object_unref); + + wwan_manager_unlock_device (device, task); +} + +static void +gsd_wwan_manager_cache_mm_object (GsdWwanManager *self, MMObject *obj) +{ + const gchar *modem_object_path; + CcWwanDevice *wwan_device; + MMModemLock lock; + + modem_object_path = g_dbus_object_get_object_path (G_DBUS_OBJECT (obj)); + g_return_if_fail (modem_object_path); + + /* This shouldn’t happen, so warn and return if this happen. */ + if (wwan_manager_find_match (self, G_DBUS_OBJECT (obj), NULL, NULL)) { + g_warning("Device %s already tracked", modem_object_path); + return; + } + + g_debug ("Tracking device at: %s", modem_object_path); + wwan_device = cc_wwan_device_new (MM_OBJECT (obj), NULL); + lock = cc_wwan_device_get_lock (wwan_device); + if (lock == MM_MODEM_LOCK_SIM_PIN || + lock == MM_MODEM_LOCK_SIM_PUK) + g_ptr_array_add (self->devices_to_unlock, wwan_device); + else + g_ptr_array_add (self->devices, wwan_device); + + g_signal_connect_object (wwan_device, "notify::unlock-required", + G_CALLBACK (wwan_manager_unlock_required_cb), + self, G_CONNECT_SWAPPED); + wwan_manager_ensure_unlocking (self); +} + + +static void +object_added_cb (GsdWwanManager *self, GDBusObject *object, GDBusObjectManager *obj_manager) +{ + g_return_if_fail (GSD_IS_WWAN_MANAGER (self)); + g_return_if_fail (G_IS_DBUS_OBJECT_MANAGER (obj_manager)); + + gsd_wwan_manager_cache_mm_object (self, MM_OBJECT(object)); +} + + +static void +object_removed_cb (GsdWwanManager *self, + GDBusObject *object, + GDBusObjectManager *obj_manager) +{ + CcWwanDevice *device; + GPtrArray *devices; + guint index; + + g_return_if_fail (GSD_IS_WWAN_MANAGER (self)); + g_return_if_fail (G_IS_DBUS_OBJECT_MANAGER (obj_manager)); + + if (!wwan_manager_find_match (self, object, &devices, &index)) + g_return_if_reached (); + + device = g_ptr_array_index (devices, index); + + g_ptr_array_remove_index (devices, index); + + if (device == self->unlocking_device) + g_cancellable_cancel (self->cancellable); +} + + +static void +mm1_name_owner_changed_cb (GDBusObjectManagerClient *client, GParamSpec *pspec, GsdWwanManager *self) +{ + g_autofree gchar *name_owner = NULL; + + name_owner = g_dbus_object_manager_client_get_name_owner (client); + self->mm1_running = !!name_owner; + g_debug ("mm name owned: %d", self->mm1_running); + + if (!self->mm1_running) { + /* Drop all devices when MM goes away */ + g_ptr_array_set_size (self->devices, 0); + g_ptr_array_set_size (self->devices_to_unlock, 0); + + g_clear_object (&self->prompt); + g_clear_pointer (&self->puk_code, gcr_secure_memory_free); + g_clear_object (&self->unlocking_device); + + return; + } +} + + +static void +get_all_modems (GsdWwanManager *self) +{ + GList *list, *l; + + g_return_if_fail (MM_IS_MANAGER (self->mm1)); + + list = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (self->mm1)); + for (l = list; l != NULL; l = l->next) + gsd_wwan_manager_cache_mm_object (self, MM_OBJECT(l->data)); + g_list_free_full (list, g_object_unref); +} + + +static void +mm1_manager_new_cb (GDBusConnection *connection, GAsyncResult *res, GsdWwanManager *self) +{ + g_autoptr(GError) error = NULL; + + self->mm1 = mm_manager_new_finish (res, &error); + if (self->mm1) { + /* Listen for added/removed modems */ + g_signal_connect_object (self->mm1, + "object-added", + G_CALLBACK (object_added_cb), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (self->mm1, + "object-removed", + G_CALLBACK (object_removed_cb), + self, + G_CONNECT_SWAPPED); + + /* Listen for name owner changes */ + g_signal_connect (self->mm1, + "notify::name-owner", + G_CALLBACK (mm1_name_owner_changed_cb), + self); + + /* Handle all modems already known to MM */ + get_all_modems (self); + } else { + g_warning ("Error connecting to D-Bus: %s", error->message); + } +} + + +static void +set_modem_manager (GsdWwanManager *self) +{ + GDBusConnection *system_bus; + g_autoptr(GError) error = NULL; + + system_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error); + if (system_bus) { + mm_manager_new (system_bus, + G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, + NULL, + (GAsyncReadyCallback) mm1_manager_new_cb, + self); + g_object_unref (system_bus); + } else { + g_warning ("Error connecting to system D-Bus: %s", error->message); + } +} + + +static gboolean +start_wwan_idle_cb (GsdWwanManager *self) +{ + g_debug ("Idle starting wwan manager"); + gnome_settings_profile_start (NULL); + + g_return_val_if_fail(GSD_IS_WWAN_MANAGER (self), FALSE); + self->settings = g_settings_new (GSD_WWAN_SCHEMA_DIR); + g_settings_bind (self->settings, "unlock-sim", self, "unlock-sim", G_SETTINGS_BIND_GET); + + set_modem_manager (self); + gnome_settings_profile_end (NULL); + self->start_idle_id = 0; + + return FALSE; +} + +gboolean +gsd_wwan_manager_start (GsdWwanManager *self, + GError **error) +{ + g_debug ("Starting wwan manager"); + g_return_val_if_fail(GSD_IS_WWAN_MANAGER (self), FALSE); + + gnome_settings_profile_start (NULL); + self->start_idle_id = g_idle_add ((GSourceFunc) start_wwan_idle_cb, self); + g_source_set_name_by_id (self->start_idle_id, "[gnome-settings-daemon] start_wwan_idle_cb"); + + gnome_settings_profile_end (NULL); + return TRUE; +} + +void +gsd_wwan_manager_stop (GsdWwanManager *self) +{ + g_debug ("Stopping wwan manager"); +} + + +static void +gsd_wwan_manager_set_unlock_sim (GsdWwanManager *self, gboolean unlock) +{ + if (self->unlock == unlock) + return; + + self->unlock = unlock; + + /* + * XXX: Should the devices in ‘self->devices’ be moved to + * ‘self->devices_to_unlock’ if required? Otherwise, no prompt + * will be shown for devices the user explicitly cancelled + * unlock prompt. + */ + /* Unlock the first device if no device is being unlocked. Unlocking + * the rest will be handled appropriately after this is finished. */ + if (self->unlock && self->devices_to_unlock->len > 0 && !self->unlocking_device) + wwan_manager_unlock_required_cb (self, NULL, + self->devices_to_unlock->pdata[0]); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_UNLOCK_SIM]); +} + + +static void +gsd_wwan_manager_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsdWwanManager *self = GSD_WWAN_MANAGER (object); + + switch (prop_id) { + case PROP_UNLOCK_SIM: + gsd_wwan_manager_set_unlock_sim (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gsd_wwan_manager_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsdWwanManager *self = GSD_WWAN_MANAGER (object); + + switch (prop_id) { + case PROP_UNLOCK_SIM: + g_value_set_boolean (value, self->unlock); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gsd_wwan_manager_dispose (GObject *object) +{ + GsdWwanManager *self = GSD_WWAN_MANAGER (object); + + if (self->mm1) { + self->mm1_running = FALSE; + g_clear_object (&self->mm1); + } + + if (self->cancellable) + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + g_clear_handle_id (&self->prompt_timeout_id, g_source_remove); + g_clear_object (&self->unlocking_device); + g_clear_pointer (&self->puk_code, gcr_secure_memory_free); + g_clear_object (&self->prompt); + + g_clear_pointer (&self->devices, g_ptr_array_unref); + g_clear_pointer (&self->devices_to_unlock, g_ptr_array_unref); + g_clear_object (&self->settings); + + G_OBJECT_CLASS (gsd_wwan_manager_parent_class)->dispose (object); +} + +static void +gsd_wwan_manager_class_init (GsdWwanManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = gsd_wwan_manager_get_property; + object_class->set_property = gsd_wwan_manager_set_property; + object_class->dispose = gsd_wwan_manager_dispose; + + props[PROP_UNLOCK_SIM] = + g_param_spec_boolean ("unlock-sim", + "unlock-sim", + "Whether to unlock new sims right away", + FALSE, + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + +static void +gsd_wwan_manager_init (GsdWwanManager *self) +{ + self->devices = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + self->devices_to_unlock = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); +} + + +GsdWwanManager * +gsd_wwan_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_WWAN_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_WWAN_MANAGER (manager_object); +} diff --git a/plugins/wwan/gsd-wwan-manager.h b/plugins/wwan/gsd-wwan-manager.h new file mode 100644 index 0000000..127d3d2 --- /dev/null +++ b/plugins/wwan/gsd-wwan-manager.h @@ -0,0 +1,36 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2019 Purism SPC + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Author: Guido Günther <agx@sigxcpu.org> + * + */ + +# pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_WWAN_MANAGER (gsd_wwan_manager_get_type()) +G_DECLARE_FINAL_TYPE (GsdWwanManager, gsd_wwan_manager, GSD, WWAN_MANAGER, GObject) + +GsdWwanManager * gsd_wwan_manager_new (void); +gboolean gsd_wwan_manager_start (GsdWwanManager *manager, + GError **error); +void gsd_wwan_manager_stop (GsdWwanManager *manager); + +G_END_DECLS diff --git a/plugins/wwan/main.c b/plugins/wwan/main.c new file mode 100644 index 0000000..c6adebd --- /dev/null +++ b/plugins/wwan/main.c @@ -0,0 +1,7 @@ +#define NEW gsd_wwan_manager_new +#define START gsd_wwan_manager_start +#define STOP gsd_wwan_manager_stop +#define MANAGER GsdWwanManager +#include "gsd-wwan-manager.h" + +#include "daemon-skeleton.h" diff --git a/plugins/wwan/meson.build b/plugins/wwan/meson.build new file mode 100644 index 0000000..3f117fb --- /dev/null +++ b/plugins/wwan/meson.build @@ -0,0 +1,21 @@ +sources = files( + 'cc-wwan-device.c', + 'gsd-wwan-manager.c', + 'main.c' +) + +deps = plugins_deps + [gio_dep, gcr_base_dep, mm_glib_dep, polkit_gobject_dep] + +cflags += ['-DGNOMECC_DATA_DIR="@0@"'.format(gsd_pkgdatadir)] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) + diff --git a/plugins/xsettings/00-xrdb b/plugins/xsettings/00-xrdb new file mode 100755 index 0000000..a047e8b --- /dev/null +++ b/plugins/xsettings/00-xrdb @@ -0,0 +1,9 @@ +#!/bin/sh + +userresources=$HOME/.Xresources +sysresources=/etc/X11/Xresources + +# merge in defaults +[ -r "$sysresources" ] && xrdb -nocpp -merge "$sysresources" +[ -r "$userresources" ] && xrdb -merge "$userresources" + diff --git a/plugins/xsettings/README.xsettings b/plugins/xsettings/README.xsettings new file mode 100644 index 0000000..624ccab --- /dev/null +++ b/plugins/xsettings/README.xsettings @@ -0,0 +1,35 @@ +This is very simple documentation for the 'override' GSettings key for +gnome-setting-daemon's xsettings plugin. + +The override is given as a dictionary of overrides to be applied on top +of the usual values that are exported to the X server as XSETTINGS. The +intent of this is to allow users to override values of programmatically +determined settings (such as 'Gtk/ShellShowsAppMenu') and to allow +developers to introduce new XSETTINGS for testing (without having to kill the +gnome-settings-daemon running in the session and run their own patched +version). + +The type of the overrides is 'a{sv}'. + +The key gives the full XSETTINGS setting name to override (for example, +'Gtk/ShellShowsAppMenu'). The value is one of the following: + + - a string ('s') for the case of a string XSETTING + + - an int32 ('i') for the case of an integer XSETTING + + - a 4-tuple of uint16s ('(qqqq)') for the case of a color XSETTING + +Dictionary items with a value that is not one of the above types will be +ignored. Specifically note that XSETTINGS does not have a concept of +booleans -- you must use an integer that is either 0 or 1. + +An example setting for this key (as expressed in GVariant text format) +might be: + + { 'Gtk/ShellShowsAppMenu': < 0 >, 'Xft/DPI': < 98304 > } + +Noting that variants must be specified in the usual way (wrapped in <>). + +Note also that DPI in the above example is expressed in 1024ths of an +inch. diff --git a/plugins/xsettings/fc-monitor.c b/plugins/xsettings/fc-monitor.c new file mode 100644 index 0000000..63e8712 --- /dev/null +++ b/plugins/xsettings/fc-monitor.c @@ -0,0 +1,317 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * Copyright (C) 2017 Jan Alexander Steffens (heftig) <jan.steffens@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Author: Behdad Esfahbod, Red Hat, Inc. + */ + +#include "fc-monitor.h" + +#include <gio/gio.h> +#include <fontconfig/fontconfig.h> + +#define TIMEOUT_MILLISECONDS 1000 + +static void +fontconfig_cache_update_thread (GTask *task, + gpointer source_object G_GNUC_UNUSED, + gpointer task_data G_GNUC_UNUSED, + GCancellable *cancellable G_GNUC_UNUSED) +{ + if (FcConfigUptoDate (NULL)) { + g_task_return_boolean (task, FALSE); + return; + } + + if (!FcInitReinitialize ()) { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, + "FcInitReinitialize failed"); + return; + } + + g_task_return_boolean (task, TRUE); +} + +static void +fontconfig_cache_update_async (GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task = g_task_new (NULL, NULL, callback, user_data); + g_task_run_in_thread (task, fontconfig_cache_update_thread); + g_object_unref (task); +} + +static gboolean +fontconfig_cache_update_finish (GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +typedef enum { + UPDATE_IDLE, + UPDATE_PENDING, + UPDATE_RUNNING, + UPDATE_RESTART, +} UpdateState; + +struct _FcMonitor { + GObject parent_instance; + + GPtrArray *monitors; + + guint timeout; + UpdateState state; + gboolean notify; +}; + +enum { + SIGNAL_UPDATED, + + N_SIGNALS +}; + +static guint signals[N_SIGNALS] = { 0, }; + +static void fc_monitor_finalize (GObject *object); +static void monitor_files (FcMonitor *self, FcStrList *list); +static void stuff_changed (GFileMonitor *monitor, GFile *file, GFile *other_file, + GFileMonitorEvent event_type, gpointer data); +static void start_timeout (FcMonitor *self); +static gboolean start_update (gpointer data); +static void update_done (GObject *source_object, GAsyncResult *result, gpointer user_data); + +G_DEFINE_TYPE (FcMonitor, fc_monitor, G_TYPE_OBJECT); + +static void +fc_monitor_class_init (FcMonitorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = fc_monitor_finalize; + + signals[SIGNAL_UPDATED] = g_signal_new ("updated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); +} + +FcMonitor * +fc_monitor_new (void) +{ + return g_object_new (FC_TYPE_MONITOR, NULL); +} + +static void +fc_monitor_init (FcMonitor *self G_GNUC_UNUSED) +{ + FcInit (); +} + +static void +fc_monitor_finalize (GObject *object) +{ + FcMonitor *self = FC_MONITOR (object); + + if (self->timeout) + g_source_remove (self->timeout); + self->timeout = 0; + + g_clear_pointer (&self->monitors, g_ptr_array_unref); + + G_OBJECT_CLASS (fc_monitor_parent_class)->finalize (object); +} + +void +fc_monitor_start (FcMonitor *self) +{ + g_return_if_fail (FC_IS_MONITOR (self)); + g_return_if_fail (self->monitors == NULL); + + self->monitors = g_ptr_array_new_with_free_func (g_object_unref); + + monitor_files (self, FcConfigGetConfigFiles (NULL)); + monitor_files (self, FcConfigGetFontDirs (NULL)); +} + +void +fc_monitor_stop (FcMonitor *self) +{ + g_return_if_fail (FC_IS_MONITOR (self)); + g_clear_pointer (&self->monitors, g_ptr_array_unref); +} + +static void +monitor_files (FcMonitor *self, + FcStrList *list) +{ + const char *str; + + while ((str = (const char *) FcStrListNext (list))) { + GFile *file; + GFileMonitor *monitor; + + file = g_file_new_for_path (str); + + monitor = g_file_monitor (file, G_FILE_MONITOR_NONE, NULL, NULL); + + g_object_unref (file); + + if (!monitor) + continue; + + g_signal_connect (monitor, "changed", G_CALLBACK (stuff_changed), self); + + g_ptr_array_add (self->monitors, monitor); + } + + FcStrListDone (list); +} + +static const gchar * +get_name (GType enum_type, + gint enum_value) +{ + GEnumClass *klass = g_type_class_ref (enum_type); + GEnumValue *value = g_enum_get_value (klass, enum_value); + const gchar *name = value ? value->value_name : "(unknown)"; + g_type_class_unref (klass); + return name; +} + +static void +stuff_changed (GFileMonitor *monitor G_GNUC_UNUSED, + GFile *file G_GNUC_UNUSED, + GFile *other_file G_GNUC_UNUSED, + GFileMonitorEvent event_type, + gpointer data) +{ + FcMonitor *self = FC_MONITOR (data); + const gchar *event_name = get_name (G_TYPE_FILE_MONITOR_EVENT, event_type); + + switch (self->state) { + case UPDATE_IDLE: + g_debug ("Got %-38s: starting fontconfig update timeout", event_name); + start_timeout (self); + break; + + case UPDATE_PENDING: + /* wait for quiescence */ + g_debug ("Got %-38s: restarting fontconfig update timeout", event_name); + g_source_remove (self->timeout); + start_timeout (self); + break; + + case UPDATE_RUNNING: + g_debug ("Got %-38s: restarting fontconfig update", event_name); + self->state = UPDATE_RESTART; + break; + + case UPDATE_RESTART: + g_debug ("Got %-38s: waiting on fontconfig update", event_name); + break; + } +} + +static void +start_timeout (FcMonitor *self) +{ + self->state = UPDATE_PENDING; + self->timeout = g_timeout_add (TIMEOUT_MILLISECONDS, start_update, self); + g_source_set_name_by_id (self->timeout, "[gnome-settings-daemon] update"); +} + +static gboolean +start_update (gpointer data) +{ + FcMonitor *self = FC_MONITOR (data); + + self->state = UPDATE_RUNNING; + self->timeout = 0; + + g_debug ("Timeout completed: starting fontconfig update"); + fontconfig_cache_update_async (update_done, g_object_ref (self)); + + return G_SOURCE_REMOVE; +} + +static void +update_done (GObject *source_object G_GNUC_UNUSED, + GAsyncResult *result, + gpointer data) +{ + FcMonitor *self = FC_MONITOR (data); + gboolean restart = self->state == UPDATE_RESTART; + GError *error = NULL; + + self->state = UPDATE_IDLE; + + if (fontconfig_cache_update_finish (result, &error)) { + g_debug ("Fontconfig update successful"); + /* Remember we had a successful update even if we have to restart it */ + self->notify = TRUE; + } else if (error) { + g_warning ("Fontconfig update failed: %s", error->message); + g_error_free (error); + } else + g_debug ("Fontconfig update was unnecessary"); + + if (restart) { + g_debug ("Concurrent change: restarting fontconfig update timeout"); + start_timeout (self); + } else if (self->notify) { + self->notify = FALSE; + + if (self->monitors) { + fc_monitor_stop (self); + fc_monitor_start (self); + } + + /* we finish modifying self before emitting the signal, + * allowing the callback to stop us if it decides to. */ + g_signal_emit (self, signals[SIGNAL_UPDATED], 0); + } + + /* release ref taken in start_update */ + g_object_unref (self); +} + +#ifdef FONTCONFIG_MONITOR_TEST +static void +yay (void) +{ + g_message ("yay"); +} + +int +main (void) +{ + GMainLoop *loop = g_main_loop_new (NULL, TRUE); + FcMonitor *monitor = fc_monitor_new (); + + fc_monitor_start (monitor); + g_signal_connect (monitor, "updated", G_CALLBACK (yay), NULL); + + g_main_loop_run (loop); + return 0; +} +#endif diff --git a/plugins/xsettings/fc-monitor.h b/plugins/xsettings/fc-monitor.h new file mode 100644 index 0000000..4b564f8 --- /dev/null +++ b/plugins/xsettings/fc-monitor.h @@ -0,0 +1,36 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2017 Jan Alexander Steffens (heftig) <jan.steffens@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ +#ifndef FC_MONITOR_H +#define FC_MONITOR_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define FC_TYPE_MONITOR (fc_monitor_get_type ()) +G_DECLARE_FINAL_TYPE (FcMonitor, fc_monitor, FC, MONITOR, GObject) + +FcMonitor *fc_monitor_new (void); + +void fc_monitor_start (FcMonitor *monitor); +void fc_monitor_stop (FcMonitor *monitor); + +G_END_DECLS + +#endif /* FC_MONITOR_H */ diff --git a/plugins/xsettings/fontconfig-test/fonts.conf b/plugins/xsettings/fontconfig-test/fonts.conf new file mode 100644 index 0000000..f9236ea --- /dev/null +++ b/plugins/xsettings/fontconfig-test/fonts.conf @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<!DOCTYPE fontconfig SYSTEM "fonts.dtd"> +<!-- /etc/fonts/fonts.conf file to configure system font access --> +<fontconfig> + +<!-- Font directory list --> + <dir>/usr/share/fonts</dir> +</fontconfig> diff --git a/plugins/xsettings/gsd-remote-display-manager.h b/plugins/xsettings/gsd-remote-display-manager.h new file mode 100644 index 0000000..3c73ab6 --- /dev/null +++ b/plugins/xsettings/gsd-remote-display-manager.h @@ -0,0 +1,28 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2012 Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <glib-object.h> + +#define GSD_TYPE_REMOTE_DISPLAY_MANAGER (gsd_remote_display_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdRemoteDisplayManager, gsd_remote_display_manager, GSD, REMOTE_DISPLAY_MANAGER, GObject) + +GsdRemoteDisplayManager * gsd_remote_display_manager_new (void); diff --git a/plugins/xsettings/gsd-xsettings-gtk.c b/plugins/xsettings/gsd-xsettings-gtk.c new file mode 100644 index 0000000..40baf41 --- /dev/null +++ b/plugins/xsettings/gsd-xsettings-gtk.c @@ -0,0 +1,384 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <glib/gi18n-lib.h> +#include <gio/gio.h> + +#include "gsd-xsettings-gtk.h" + +#define XSETTINGS_PLUGIN_SCHEMA "org.gnome.settings-daemon.plugins.xsettings" + +#define GTK_MODULES_DISABLED_KEY "disabled-gtk-modules" +#define GTK_MODULES_ENABLED_KEY "enabled-gtk-modules" + +static const char *modules_path = NULL; + +enum { + PROP_0, + PROP_GTK_MODULES +}; + +struct _GsdXSettingsGtk { + GObject parent; + + char *modules; + GHashTable *dir_modules; + + GSettings *settings; + + guint64 dir_mtime; + GFileMonitor *monitor; + GList *cond_settings; +}; + +G_DEFINE_TYPE(GsdXSettingsGtk, gsd_xsettings_gtk, G_TYPE_OBJECT) + +static void update_gtk_modules (GsdXSettingsGtk *gtk); + +static void +empty_cond_settings_list (GsdXSettingsGtk *gtk) +{ + if (gtk->cond_settings == NULL) + return; + + /* Empty the list of settings */ + g_list_foreach (gtk->cond_settings, (GFunc) g_object_unref, NULL); + g_list_free (gtk->cond_settings); + gtk->cond_settings = NULL; +} + +static void +cond_setting_changed (GSettings *settings, + const char *key, + GsdXSettingsGtk *gtk) +{ + gboolean enabled; + const char *module_name; + + module_name = g_object_get_data (G_OBJECT (settings), "module-name"); + + enabled = g_settings_get_boolean (settings, key); + if (enabled != FALSE) { + if (gtk->dir_modules == NULL) + gtk->dir_modules = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + g_hash_table_insert (gtk->dir_modules, g_strdup (module_name), NULL); + } else if (gtk->dir_modules != NULL) { + g_hash_table_remove (gtk->dir_modules, module_name); + } + + update_gtk_modules (gtk); +} + +static char * +process_desktop_file (const char *path, + GsdXSettingsGtk *gtk) +{ + GKeyFile *keyfile; + char *retval; + char *module_name; + + retval = NULL; + + if (g_str_has_suffix (path, ".desktop") == FALSE && + g_str_has_suffix (path, ".gtk-module") == FALSE) + return retval; + + keyfile = g_key_file_new (); + if (g_key_file_load_from_file (keyfile, path, G_KEY_FILE_NONE, NULL) == FALSE) + goto bail; + + if (g_key_file_has_group (keyfile, "GTK Module") == FALSE) + goto bail; + + module_name = g_key_file_get_string (keyfile, "GTK Module", "X-GTK-Module-Name", NULL); + if (module_name == NULL) + goto bail; + + if (g_key_file_has_key (keyfile, "GTK Module", "X-GTK-Module-Enabled-Schema", NULL) != FALSE) { + char *schema; + char *key; + gboolean enabled; + GSettings *settings; + char *signal; + + schema = g_key_file_get_string (keyfile, "GTK Module", "X-GTK-Module-Enabled-Schema", NULL); + key = g_key_file_get_string (keyfile, "GTK Module", "X-GTK-Module-Enabled-Key", NULL); + + settings = g_settings_new (schema); + + gtk->cond_settings = g_list_prepend (gtk->cond_settings, settings); + + g_object_set_data_full (G_OBJECT (settings), "module-name", g_strdup (module_name), (GDestroyNotify) g_free); + + signal = g_strdup_printf ("changed::%s", key); + g_signal_connect_object (G_OBJECT (settings), signal, G_CALLBACK (cond_setting_changed), gtk, 0); + enabled = g_settings_get_boolean (settings, key); + g_free (signal); + g_free (schema); + g_free (key); + + if (enabled != FALSE) + retval = g_strdup (module_name); + } else { + retval = g_strdup (module_name); + } + + g_free (module_name); + +bail: + g_key_file_free (keyfile); + return retval; +} + +static void +get_gtk_modules_from_dir (GsdXSettingsGtk *gtk) +{ + GFile *file; + GFileInfo *info; + GHashTable *ht; + + file = g_file_new_for_path (modules_path); + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NONE, + NULL, + NULL); + if (info != NULL) { + guint64 dir_mtime; + + dir_mtime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED); + if (gtk->dir_mtime == 0 || + dir_mtime > gtk->dir_mtime) { + GDir *dir; + const char *name; + + empty_cond_settings_list (gtk); + + gtk->dir_mtime = dir_mtime; + + if (gtk->dir_modules != NULL) { + g_hash_table_destroy (gtk->dir_modules); + gtk->dir_modules = NULL; + } + + dir = g_dir_open (modules_path, 0, NULL); + if (dir == NULL) + goto bail; + + ht = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + while ((name = g_dir_read_name (dir)) != NULL) { + char *path; + char *module; + + path = g_build_filename (modules_path, name, NULL); + module = process_desktop_file (path, gtk); + if (module != NULL) + g_hash_table_insert (ht, module, NULL); + g_free (path); + } + g_dir_close (dir); + + gtk->dir_modules = ht; + } + g_object_unref (info); + } else { + empty_cond_settings_list (gtk); + } + +bail: + g_object_unref (file); +} + +static void +stringify_gtk_modules (gpointer key, + gpointer value, + GString *str) +{ + if (str->len != 0) + g_string_append_c (str, ':'); + g_string_append (str, key); +} + +static void +update_gtk_modules (GsdXSettingsGtk *gtk) +{ + char **enabled, **disabled; + GHashTable *ht; + guint i; + GString *str; + char *modules; + + enabled = g_settings_get_strv (gtk->settings, GTK_MODULES_ENABLED_KEY); + disabled = g_settings_get_strv (gtk->settings, GTK_MODULES_DISABLED_KEY); + + ht = g_hash_table_new (g_str_hash, g_str_equal); + + if (gtk->dir_modules != NULL) { + GList *list, *l; + + list = g_hash_table_get_keys (gtk->dir_modules); + for (l = list; l != NULL; l = l->next) { + g_hash_table_insert (ht, l->data, NULL); + } + g_list_free (list); + } + + for (i = 0; enabled[i] != NULL; i++) + g_hash_table_insert (ht, enabled[i], NULL); + + for (i = 0; disabled[i] != NULL; i++) + g_hash_table_remove (ht, disabled[i]); + + str = g_string_new (NULL); + g_hash_table_foreach (ht, (GHFunc) stringify_gtk_modules, str); + g_hash_table_destroy (ht); + + modules = g_string_free (str, FALSE); + + if (modules == NULL || + gtk->modules == NULL || + g_str_equal (modules, gtk->modules) == FALSE) { + g_free (gtk->modules); + gtk->modules = modules; + g_object_notify (G_OBJECT (gtk), "gtk-modules"); + } else { + g_free (modules); + } + + g_strfreev (enabled); + g_strfreev (disabled); +} + +static void +gtk_modules_dir_changed_cb (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + GsdXSettingsGtk *gtk) +{ + get_gtk_modules_from_dir (gtk); + update_gtk_modules (gtk); +} + +static void +gsd_xsettings_gtk_init (GsdXSettingsGtk *gtk) +{ + GFile *file; + + g_debug ("GsdXSettingsGtk initializing"); + + gtk->settings = g_settings_new (XSETTINGS_PLUGIN_SCHEMA); + + modules_path = g_getenv ("GSD_gtk_modules_dir"); + if (modules_path == NULL) + modules_path = GTK_MODULES_DIRECTORY; + + get_gtk_modules_from_dir (gtk); + + file = g_file_new_for_path (modules_path); + gtk->monitor = g_file_monitor (file, + G_FILE_MONITOR_NONE, + NULL, + NULL); + g_signal_connect (G_OBJECT (gtk->monitor), "changed", + G_CALLBACK (gtk_modules_dir_changed_cb), gtk); + g_object_unref (file); + + update_gtk_modules (gtk); +} + +static void +gsd_xsettings_gtk_finalize (GObject *object) +{ + GsdXSettingsGtk *gtk; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_XSETTINGS_GTK (object)); + + g_debug ("GsdXSettingsGtk finalizing"); + + gtk = GSD_XSETTINGS_GTK (object); + + g_return_if_fail (gtk != NULL); + + g_free (gtk->modules); + gtk->modules = NULL; + + if (gtk->dir_modules != NULL) { + g_hash_table_destroy (gtk->dir_modules); + gtk->dir_modules = NULL; + } + + g_object_unref (gtk->settings); + + if (gtk->monitor != NULL) + g_object_unref (gtk->monitor); + + empty_cond_settings_list (gtk); + + G_OBJECT_CLASS (gsd_xsettings_gtk_parent_class)->finalize (object); +} + +static void +gsd_xsettings_gtk_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsdXSettingsGtk *self; + + self = GSD_XSETTINGS_GTK (object); + + switch (prop_id) { + case PROP_GTK_MODULES: + g_value_set_string (value, self->modules); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gsd_xsettings_gtk_class_init (GsdXSettingsGtkClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = gsd_xsettings_gtk_get_property; + object_class->finalize = gsd_xsettings_gtk_finalize; + + g_object_class_install_property (object_class, PROP_GTK_MODULES, + g_param_spec_string ("gtk-modules", NULL, NULL, + NULL, G_PARAM_READABLE)); +} + +GsdXSettingsGtk * +gsd_xsettings_gtk_new (void) +{ + return GSD_XSETTINGS_GTK (g_object_new (GSD_TYPE_XSETTINGS_GTK, NULL)); +} + +const char * +gsd_xsettings_gtk_get_modules (GsdXSettingsGtk *gtk) +{ + return gtk->modules; +} diff --git a/plugins/xsettings/gsd-xsettings-gtk.h b/plugins/xsettings/gsd-xsettings-gtk.h new file mode 100644 index 0000000..13f6b88 --- /dev/null +++ b/plugins/xsettings/gsd-xsettings-gtk.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2010 Bastien Nocera <hadess@hadess.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_XSETTINGS_GTK_H__ +#define __GSD_XSETTINGS_GTK_H__ + +#include <glib.h> +#include <glib-object.h> +#include <gmodule.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_XSETTINGS_GTK (gsd_xsettings_gtk_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdXSettingsGtk, gsd_xsettings_gtk, GSD, XSETTINGS_GTK, GObject) + +GsdXSettingsGtk *gsd_xsettings_gtk_new (void); + +const char * gsd_xsettings_gtk_get_modules (GsdXSettingsGtk *gtk); + +G_END_DECLS + +#endif /* __GSD_XSETTINGS_GTK_H__ */ diff --git a/plugins/xsettings/gsd-xsettings-manager.c b/plugins/xsettings/gsd-xsettings-manager.c new file mode 100644 index 0000000..7b6fe43 --- /dev/null +++ b/plugins/xsettings/gsd-xsettings-manager.c @@ -0,0 +1,1588 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 Rodrigo Moya + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <sys/types.h> +#include <sys/wait.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <time.h> + +#include <X11/Xatom.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <gdk/gdk.h> +#include <gdk/gdkx.h> +#include <gtk/gtk.h> + +#include "gnome-settings-profile.h" +#include "gsd-enums.h" +#include "gsd-xsettings-manager.h" +#include "gsd-xsettings-gtk.h" +#include "gnome-settings-bus.h" +#include "xsettings-manager.h" +#include "fc-monitor.h" +#include "gsd-remote-display-manager.h" +#include "wm-button-layout-translation.h" + +#define MOUSE_SETTINGS_SCHEMA "org.gnome.settings-daemon.peripherals.mouse" +#define BACKGROUND_SETTINGS_SCHEMA "org.gnome.desktop.background" +#define INTERFACE_SETTINGS_SCHEMA "org.gnome.desktop.interface" +#define SOUND_SETTINGS_SCHEMA "org.gnome.desktop.sound" +#define PRIVACY_SETTINGS_SCHEMA "org.gnome.desktop.privacy" +#define WM_SETTINGS_SCHEMA "org.gnome.desktop.wm.preferences" +#define A11Y_SCHEMA "org.gnome.desktop.a11y" +#define CLASSIC_WM_SETTINGS_SCHEMA "org.gnome.shell.extensions.classic-overrides" + +#define XSETTINGS_PLUGIN_SCHEMA "org.gnome.settings-daemon.plugins.xsettings" +#define XSETTINGS_OVERRIDE_KEY "overrides" + +#define GTK_MODULES_DISABLED_KEY "disabled-gtk-modules" +#define GTK_MODULES_ENABLED_KEY "enabled-gtk-modules" + +#define TEXT_SCALING_FACTOR_KEY "text-scaling-factor" +#define CURSOR_SIZE_KEY "cursor-size" +#define CURSOR_THEME_KEY "cursor-theme" + +#define FONT_ANTIALIASING_KEY "antialiasing" +#define FONT_HINTING_KEY "hinting" +#define FONT_RGBA_ORDER_KEY "rgba-order" + +#define GTK_SETTINGS_DBUS_PATH "/org/gtk/Settings" +#define GTK_SETTINGS_DBUS_NAME "org.gtk.Settings" + +static const gchar introspection_xml[] = +"<node name='/org/gtk/Settings'>" +" <interface name='org.gtk.Settings'>" +" <property name='FontconfigTimestamp' type='x' access='read'/>" +" <property name='Modules' type='s' access='read'/>" +" <property name='EnableAnimations' type='b' access='read'/>" +" </interface>" +"</node>"; + +/* As we cannot rely on the X server giving us good DPI information, and + * that we don't want multi-monitor screens to have different DPIs (thus + * different text sizes), we'll hard-code the value of the DPI + * + * See also: + * https://bugzilla.novell.com/show_bug.cgi?id=217790• + * https://bugzilla.gnome.org/show_bug.cgi?id=643704 + * + * http://lists.fedoraproject.org/pipermail/devel/2011-October/157671.html + * Why EDID is not trustworthy for DPI + * Adam Jackson ajax at redhat.com + * Tue Oct 4 17:54:57 UTC 2011 + * + * Previous message: GNOME 3 - font point sizes now scaled? + * Next message: Why EDID is not trustworthy for DPI + * Messages sorted by: [ date ] [ thread ] [ subject ] [ author ] + * + * On Tue, 2011-10-04 at 11:46 -0400, Kaleb S. KEITHLEY wrote: + * + * > Grovelling around in the F15 xorg-server sources and reviewing the Xorg + * > log file on my F15 box, I see, with _modern hardware_ at least, that we + * > do have the monitor geometry available from DDC or EDIC, and obviously + * > it is trivial to compute the actual, correct DPI for each screen. + * + * I am clearly going to have to explain this one more time, forever. + * Let's see if I can't write it authoritatively once and simply answer + * with a URL from here out. (As always, use of the second person "you" + * herein is plural, not singular.) + * + * EDID does not reliably give you the size of the display. + * + * Base EDID has at least two different places where you can give a + * physical size (before considering extensions that aren't widely deployed + * so whatever). The first is a global property, measured in centimeters, + * of the physical size of the glass. The second is attached to your (zero + * or more) detailed timing specifications, and reflects the size of the + * mode, in millimeters. + * + * So, how does this screw you? + * + * a) Glass size is too coarse. On a large display that cm roundoff isn't + * a big deal, but on subnotebooks it's a different game. The 11" MBA is + * 25.68x14.44 cm, so that gives you a range of 52.54-54.64 dpcm horizontal + * and 51.20-54.86 dpcm vertical (133.4-138.8 dpi h and 130.0-139.3 dpi v). + * Which is optimistic, because that's doing the math forward from knowing + * the actual size, and you as the EDID parser can't know which way the + * manufacturer rounded. + * + * b) Glass size need not be non-zero. This is in fact the usual case for + * projectors, which don't have a fixed display size since it's a function + * of how far away the wall is from the lens. + * + * c) Glass size could be partially non-zero. Yes, really. EDID 1.4 + * defines a method of using these two bytes to encode aspect ratio, where + * if vertical size is 0 then the aspect ratio is computed as (horizontal + * value + 99) / 100 in portrait mode (and the obvious reverse thing if + * horizontal is zero). Admittedly, unlike every other item in this list, + * I've never seen this in the wild. But it's legal. + * + * d) Glass size could be a direct encoding of the aspect ratio. Base EDID + * doesn't condone this behaviour, but the CEA spec (to which all HDMI + * monitors must conform) does allow-but-not-require it, which means your + * 1920x1080 TV could claim to be 16 "cm" by 9 "cm". So of course that's + * what TV manufacturers do because that way they don't have to modify the + * EDID info when physical construction changes, and that's cheaper. + * + * e) You could use mode size to get size in millimeters, but you might not + * have any detailed timings. + * + * f) You could use mode size, but mode size is explicitly _not_ glass + * size. It's the size that the display chooses to present that mode. + * Sometimes those are the same, and sometimes they're not. You could be + * scaled or {letter,pillar}boxed, and that's not necessarily something you + * can control from the host side. + * + * g) You could use mode size, but it could be an encoded aspect ratio, as + * in case d above, because CEA says that's okay. + * + * h) You could use mode size, but it could be the aspect ratio from case d + * multiplied by 10 in each direction (because, of course, you gave size in + * centimeters and so your authoring tool just multiplied it up). + * + * i) Any or all of the above could be complete and utter garbage, because + * - and I really, really need you to understand this - there is no + * requirements program for any commercial OS or industry standard that + * requires honesty here, as far as I'm aware. There is every incentive + * for there to _never_ be one, because it would make the manufacturing + * process more expensive. + * + * So from this point the suggestion is usually "well come up with some + * heuristic to make a good guess assuming there's some correlation between + * the various numbers you're given". I have in fact written heuristics + * for this, and they're in your kernel and your X server, and they still + * encounter a huge number of cases where we simply _cannot_ know from EDID + * anything like a physical size, because - to pick only one example - the + * consumer electronics industry are cheap bastards, because you the + * consumer demanded that they be cheap. + * + * And then your only recourse is to an external database, and now you're + * up the creek again because the identifying information here is a + * vendor/model/serial tuple, and the vendor can and does change physical + * construction without changing model number. Now you get to play the + * guessing game of how big the serial number range is for each subvariant, + * assuming they bothered to encode a serial number - and they didn't. Or, + * if they bothered to encode week/year of manufacturer correctly - and + * they didn't - which weeks meant which models. And then you still have + * to go out and buy one of every TV at Fry's, and that covers you for one + * market, for three months. + * + * If someone wants to write something better, please, by all means. If + * it's kernel code, send it to dri-devel at lists.freedesktop.org and cc me + * and I will happily review it. Likewise xorg-devel@ for X server + * changes. + * + * I gently suggest that doing so is a waste of time. + * + * But if there's one thing free software has taught me, it's that you can + * not tell people something is a bad idea and have any expectation they + * will believe you. + * + * > Obviously in a multi-screen set-up using Xinerama this has the potential + * > to be a Hard Problem if the monitors differ greatly in their DPI. + * > + * > If the major resistance is over what to do with older hardware that + * > doesn't have this data available, then yes, punt; use a hard-coded + * > default. Likewise, if the two monitors really differ greatly, then punt. + * + * I'm going to limit myself to observing that "greatly" is a matter of + * opinion, and that in order to be really useful you'd need some way of + * communicating "I punted" to the desktop. + * + * Beyond that, sure, pick a heuristic, accept that it's going to be + * insufficient for someone, and then sit back and wait to get + * second-guessed on it over and over. + * + * > And it wouldn't be so hard to to add something like -dpi:0, -dpi:1, + * > -dpi:2 command line options to specify per-screen dpi. I kinda thought I + * > did that a long, long time ago, but maybe I only thought about doing it + * > and never actually got around to it. + * + * The RANDR extension as of version 1.2 does allow you to override + * physical size on a per-output basis at runtime. We even try pretty hard + * to set them as honestly as we can up front. The 96dpi thing people + * complain about is from the per-screen info, which is simply a default + * because of all the tl;dr above; because you have N outputs per screen + * which means a single number is in general useless; and because there is + * no way to refresh the per-screen info at runtime, as it's only ever sent + * in the initial connection handshake. + * + * - ajax + * + */ +#define DPI_FALLBACK 96 + +typedef struct _TranslationEntry TranslationEntry; +typedef void (* TranslationFunc) (GsdXSettingsManager *manager, + TranslationEntry *trans, + GVariant *value); + +struct _TranslationEntry { + const char *gsettings_schema; + const char *gsettings_key; + const char *xsetting_name; + + TranslationFunc translate; +}; + +typedef struct _FixedEntry FixedEntry; +typedef void (* FixedFunc) (GsdXSettingsManager *manager, + FixedEntry *fixed); +typedef union { + const char *str; + int num; +} FixedEntryValue; + +struct _FixedEntry { + const char *xsetting_name; + FixedFunc func; + FixedEntryValue val; +}; + +struct _GsdXSettingsManager +{ + GObject parent; + + guint start_idle_id; + XSettingsManager *manager; + GHashTable *settings; + + GSettings *plugin_settings; + FcMonitor *fontconfig_monitor; + gint64 fontconfig_timestamp; + + GsdXSettingsGtk *gtk; + + guint introspect_properties_changed_id; + guint shell_introspect_watch_id; + gboolean enable_animations; + + guint display_config_watch_id; + guint monitors_changed_id; + + guint shell_name_watch_id; + gboolean have_shell; + + guint notify_idle_id; + + GDBusNodeInfo *introspection_data; + GDBusConnection *dbus_connection; + guint gtk_settings_name_id; +}; + +#define GSD_XSETTINGS_ERROR gsd_xsettings_error_quark () + +enum { + GSD_XSETTINGS_ERROR_INIT +}; + +static void gsd_xsettings_manager_class_init (GsdXSettingsManagerClass *klass); +static void gsd_xsettings_manager_init (GsdXSettingsManager *xsettings_manager); +static void gsd_xsettings_manager_finalize (GObject *object); + +static void register_manager_dbus (GsdXSettingsManager *manager); + +G_DEFINE_TYPE (GsdXSettingsManager, gsd_xsettings_manager, G_TYPE_OBJECT) + +static gpointer manager_object = NULL; + +static GQuark +gsd_xsettings_error_quark (void) +{ + return g_quark_from_static_string ("gsd-xsettings-error-quark"); +} + +static void +translate_bool_int (GsdXSettingsManager *manager, + TranslationEntry *trans, + GVariant *value) +{ + xsettings_manager_set_int (manager->manager, trans->xsetting_name, + g_variant_get_boolean (value)); +} + +static void +translate_int_int (GsdXSettingsManager *manager, + TranslationEntry *trans, + GVariant *value) +{ + xsettings_manager_set_int (manager->manager, trans->xsetting_name, + g_variant_get_int32 (value)); +} + +static void +translate_string_string (GsdXSettingsManager *manager, + TranslationEntry *trans, + GVariant *value) +{ + xsettings_manager_set_string (manager->manager, + trans->xsetting_name, + g_variant_get_string (value, NULL)); +} + +static void +translate_button_layout (GsdXSettingsManager *manager, + TranslationEntry *trans, + GVariant *value) +{ + GSettings *classic_settings; + GVariant *classic_value = NULL; + char *layout; + + /* Hack: until we get session-dependent defaults in GSettings, + * swap out the usual schema for the "classic" one when + * running in classic mode + */ + classic_settings = g_hash_table_lookup (manager->settings, + CLASSIC_WM_SETTINGS_SCHEMA); + if (classic_settings) { + classic_value = g_settings_get_value (classic_settings, "button-layout"); + layout = g_variant_dup_string (classic_value, NULL); + } else { + layout = g_variant_dup_string (value, NULL); + } + + translate_wm_button_layout_to_gtk (layout); + + xsettings_manager_set_string (manager->manager, + trans->xsetting_name, + layout); + + if (classic_value) + g_variant_unref (classic_value); + g_free (layout); +} + +static void +fixed_false_int (GsdXSettingsManager *manager, + FixedEntry *fixed) +{ + xsettings_manager_set_int (manager->manager, fixed->xsetting_name, FALSE); +} + +static void +fixed_true_int (GsdXSettingsManager *manager, + FixedEntry *fixed) +{ + xsettings_manager_set_int (manager->manager, fixed->xsetting_name, TRUE); +} + +static void +fixed_bus_id (GsdXSettingsManager *manager, + FixedEntry *fixed) +{ + const gchar *id; + GDBusConnection *bus; + GVariant *res; + + bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + res = g_dbus_connection_call_sync (bus, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "GetId", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL); + + if (res) { + g_variant_get (res, "(&s)", &id); + + xsettings_manager_set_string (manager->manager, fixed->xsetting_name, id); + g_variant_unref (res); + } + + g_object_unref (bus); +} + +static void +fixed_string (GsdXSettingsManager *manager, + FixedEntry *fixed) +{ + xsettings_manager_set_string (manager->manager, + fixed->xsetting_name, + fixed->val.str); +} + +static void +fixed_int (GsdXSettingsManager *manager, + FixedEntry *fixed) +{ + xsettings_manager_set_int (manager->manager, + fixed->xsetting_name, + fixed->val.num); +} + +#define DEFAULT_COLOR_PALETTE "black:white:gray50:red:purple:blue:light blue:green:yellow:orange:lavender:brown:goldenrod4:dodger blue:pink:light green:gray10:gray30:gray75:gray90" + +static FixedEntry fixed_entries [] = { + { "Gtk/MenuImages", fixed_false_int }, + { "Gtk/ButtonImages", fixed_false_int }, + { "Gtk/ShowInputMethodMenu", fixed_false_int }, + { "Gtk/ShowUnicodeMenu", fixed_false_int }, + { "Gtk/AutoMnemonics", fixed_true_int }, + { "Gtk/DialogsUseHeader", fixed_true_int }, + { "Gtk/SessionBusId", fixed_bus_id }, + { "Gtk/ShellShowsAppMenu", fixed_false_int }, + { "Gtk/ColorPalette", fixed_string, { .str = DEFAULT_COLOR_PALETTE } }, + { "Net/FallbackIconTheme", fixed_string, { .str = "gnome" } }, + { "Gtk/ToolbarStyle", fixed_string, { .str = "both-horiz" } }, + { "Gtk/ToolbarIconSize", fixed_string, { .str = "large" } }, + { "Gtk/CanChangeAccels", fixed_false_int }, + { "Gtk/TimeoutInitial", fixed_int, { .num = 200 } }, + { "Gtk/TimeoutRepeat", fixed_int, { .num = 20 } }, + { "Gtk/ColorScheme", fixed_string, { .str = "" } }, + { "Gtk/IMPreeditStyle", fixed_string, { .str = "callback" } }, + { "Gtk/IMStatusStyle", fixed_string, { .str = "callback" } }, + { "Gtk/MenuBarAccel", fixed_string, { .str = "F10" } } +}; + +static TranslationEntry translations [] = { + { "org.gnome.settings-daemon.peripherals.mouse", "double-click", "Net/DoubleClickTime", translate_int_int }, + { "org.gnome.settings-daemon.peripherals.mouse", "drag-threshold", "Net/DndDragThreshold", translate_int_int }, + + { "org.gnome.desktop.background", "show-desktop-icons", "Gtk/ShellShowsDesktop", translate_bool_int }, + + { "org.gnome.desktop.interface", "font-name", "Gtk/FontName", translate_string_string }, + { "org.gnome.desktop.interface", "gtk-key-theme", "Gtk/KeyThemeName", translate_string_string }, + { "org.gnome.desktop.interface", "cursor-blink", "Net/CursorBlink", translate_bool_int }, + { "org.gnome.desktop.interface", "cursor-blink-time", "Net/CursorBlinkTime", translate_int_int }, + { "org.gnome.desktop.interface", "cursor-blink-timeout", "Gtk/CursorBlinkTimeout", translate_int_int }, + { "org.gnome.desktop.interface", "gtk-theme", "Net/ThemeName", translate_string_string }, + { "org.gnome.desktop.interface", "gtk-im-module", "Gtk/IMModule", translate_string_string }, + { "org.gnome.desktop.interface", "icon-theme", "Net/IconThemeName", translate_string_string }, + { "org.gnome.desktop.interface", "cursor-theme", "Gtk/CursorThemeName", translate_string_string }, + { "org.gnome.desktop.interface", "gtk-enable-primary-paste", "Gtk/EnablePrimaryPaste", translate_bool_int }, + { "org.gnome.desktop.interface", "overlay-scrolling", "Gtk/OverlayScrolling", translate_bool_int }, + /* cursor-size is handled via the Xft side as it needs the scaling factor */ + + { "org.gnome.desktop.sound", "theme-name", "Net/SoundThemeName", translate_string_string }, + { "org.gnome.desktop.sound", "event-sounds", "Net/EnableEventSounds" , translate_bool_int }, + { "org.gnome.desktop.sound", "input-feedback-sounds", "Net/EnableInputFeedbackSounds", translate_bool_int }, + + { "org.gnome.desktop.privacy", "recent-files-max-age", "Gtk/RecentFilesMaxAge", translate_int_int }, + { "org.gnome.desktop.privacy", "remember-recent-files", "Gtk/RecentFilesEnabled", translate_bool_int }, + { "org.gnome.desktop.wm.preferences", "button-layout", "Gtk/DecorationLayout", translate_button_layout }, + { "org.gnome.desktop.wm.preferences", "action-double-click-titlebar", "Gtk/TitlebarDoubleClick", translate_string_string }, + { "org.gnome.desktop.wm.preferences", "action-middle-click-titlebar", "Gtk/TitlebarMiddleClick", translate_string_string }, + { "org.gnome.desktop.wm.preferences", "action-right-click-titlebar", "Gtk/TitlebarRightClick", translate_string_string }, + { "org.gnome.desktop.a11y", "always-show-text-caret", "Gtk/KeynavUseCaret", translate_bool_int } +}; + +static gboolean +notify_idle (gpointer data) +{ + GsdXSettingsManager *manager = data; + + xsettings_manager_notify (manager->manager); + + manager->notify_idle_id = 0; + return G_SOURCE_REMOVE; +} + +static void +queue_notify (GsdXSettingsManager *manager) +{ + if (manager->notify_idle_id != 0) + return; + + manager->notify_idle_id = g_idle_add (notify_idle, manager); + g_source_set_name_by_id (manager->notify_idle_id, "[gnome-settings-daemon] notify_idle"); +} + +typedef enum { + GTK_SETTINGS_FONTCONFIG_TIMESTAMP = 1 << 0, + GTK_SETTINGS_MODULES = 1 << 1, + GTK_SETTINGS_ENABLE_ANIMATIONS = 1 << 2 +} GtkSettingsMask; + +static void +send_dbus_event (GsdXSettingsManager *manager, + GtkSettingsMask mask) +{ + GVariantBuilder props_builder; + GVariant *props_changed = NULL; + + g_variant_builder_init (&props_builder, G_VARIANT_TYPE ("a{sv}")); + + if (mask & GTK_SETTINGS_FONTCONFIG_TIMESTAMP) { + g_variant_builder_add (&props_builder, "{sv}", "FontconfigTimestamp", + g_variant_new_int64 (manager->fontconfig_timestamp)); + } + + if (mask & GTK_SETTINGS_MODULES) { + const char *modules = gsd_xsettings_gtk_get_modules (manager->gtk); + g_variant_builder_add (&props_builder, "{sv}", "Modules", + g_variant_new_string (modules ? modules : "")); + } + + if (mask & GTK_SETTINGS_ENABLE_ANIMATIONS) { + g_variant_builder_add (&props_builder, "{sv}", "EnableAnimations", + g_variant_new_boolean (manager->enable_animations)); + } + + props_changed = g_variant_new ("(s@a{sv}@as)", GTK_SETTINGS_DBUS_NAME, + g_variant_builder_end (&props_builder), + g_variant_new_strv (NULL, 0)); + + g_dbus_connection_emit_signal (manager->dbus_connection, + NULL, + GTK_SETTINGS_DBUS_PATH, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + props_changed, NULL); +} + +static double +get_dpi_from_gsettings (GsdXSettingsManager *manager) +{ + GSettings *interface_settings; + double dpi; + double factor; + + interface_settings = g_hash_table_lookup (manager->settings, INTERFACE_SETTINGS_SCHEMA); + factor = g_settings_get_double (interface_settings, TEXT_SCALING_FACTOR_KEY); + + dpi = DPI_FALLBACK; + + return dpi * factor; +} + +static gboolean +get_legacy_ui_scale (GVariantIter *properties, + int *scale) +{ + const char *key; + GVariant *value; + + *scale = 0; + + while (g_variant_iter_loop (properties, "{&sv}", &key, &value)) { + if (!g_str_equal (key, "legacy-ui-scaling-factor")) + continue; + + *scale = g_variant_get_int32 (value); + break; + } + + if (*scale < 1) { + g_warning ("Failed to get current UI legacy scaling factor"); + *scale = 1; + return FALSE; + } + + return TRUE; +} + +#define MODE_FORMAT "(siiddada{sv})" +#define MODES_FORMAT "a" MODE_FORMAT + +#define MONITOR_SPEC_FORMAT "(ssss)" +#define MONITOR_FORMAT "(" MONITOR_SPEC_FORMAT MODES_FORMAT "a{sv})" +#define MONITORS_FORMAT "a" MONITOR_FORMAT + +#define LOGICAL_MONITOR_FORMAT "(iiduba" MONITOR_SPEC_FORMAT "a{sv})" +#define LOGICAL_MONITORS_FORMAT "a" LOGICAL_MONITOR_FORMAT + +#define CURRENT_STATE_FORMAT "(u" MONITORS_FORMAT LOGICAL_MONITORS_FORMAT "a{sv})" + +static int +get_window_scale (GsdXSettingsManager *manager) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) current_state = NULL; + g_autoptr(GVariantIter) properties = NULL; + int scale = 1; + + current_state = + g_dbus_connection_call_sync (manager->dbus_connection, + "org.gnome.Mutter.DisplayConfig", + "/org/gnome/Mutter/DisplayConfig", + "org.gnome.Mutter.DisplayConfig", + "GetCurrentState", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + NULL, + &error); + if (!current_state) { + g_warning ("Failed to get current display configuration state: %s", + error->message); + return 1; + } + + g_variant_get (current_state, + CURRENT_STATE_FORMAT, + NULL, + NULL, + NULL, + &properties); + + if (!get_legacy_ui_scale (properties, &scale)) + g_warning ("Failed to get current UI legacy scaling factor"); + + return scale; +} + +typedef struct { + gboolean antialias; + gboolean hinting; + int scaled_dpi; + int dpi; + int window_scale; + int cursor_size; + char *cursor_theme; + const char *rgba; + const char *hintstyle; +} GsdXftSettings; + +/* Read GSettings and determine the appropriate Xft settings based on them. */ +static void +xft_settings_get (GsdXSettingsManager *manager, + GsdXftSettings *settings) +{ + GSettings *interface_settings; + GsdFontAntialiasingMode antialiasing; + GsdFontHinting hinting; + GsdFontRgbaOrder order; + gboolean use_rgba = FALSE; + double dpi; + int cursor_size; + + interface_settings = g_hash_table_lookup (manager->settings, INTERFACE_SETTINGS_SCHEMA); + + antialiasing = g_settings_get_enum (manager->plugin_settings, FONT_ANTIALIASING_KEY); + hinting = g_settings_get_enum (manager->plugin_settings, FONT_HINTING_KEY); + order = g_settings_get_enum (manager->plugin_settings, FONT_RGBA_ORDER_KEY); + + settings->antialias = (antialiasing != GSD_FONT_ANTIALIASING_MODE_NONE); + settings->hinting = (hinting != GSD_FONT_HINTING_NONE); + settings->window_scale = get_window_scale (manager); + dpi = get_dpi_from_gsettings (manager); + settings->dpi = dpi * 1024; /* Xft wants 1/1024ths of an inch */ + settings->scaled_dpi = dpi * settings->window_scale * 1024; + cursor_size = g_settings_get_int (interface_settings, CURSOR_SIZE_KEY); + settings->cursor_size = cursor_size * settings->window_scale; + settings->cursor_theme = g_settings_get_string (interface_settings, CURSOR_THEME_KEY); + settings->rgba = "rgb"; + settings->hintstyle = "hintfull"; + + switch (hinting) { + case GSD_FONT_HINTING_NONE: + settings->hintstyle = "hintnone"; + break; + case GSD_FONT_HINTING_SLIGHT: + settings->hintstyle = "hintslight"; + break; + case GSD_FONT_HINTING_MEDIUM: + settings->hintstyle = "hintmedium"; + break; + case GSD_FONT_HINTING_FULL: + settings->hintstyle = "hintfull"; + break; + } + + switch (order) { + case GSD_FONT_RGBA_ORDER_RGBA: + settings->rgba = "rgba"; + break; + case GSD_FONT_RGBA_ORDER_RGB: + settings->rgba = "rgb"; + break; + case GSD_FONT_RGBA_ORDER_BGR: + settings->rgba = "bgr"; + break; + case GSD_FONT_RGBA_ORDER_VRGB: + settings->rgba = "vrgb"; + break; + case GSD_FONT_RGBA_ORDER_VBGR: + settings->rgba = "vbgr"; + break; + } + + switch (antialiasing) { + case GSD_FONT_ANTIALIASING_MODE_NONE: + settings->antialias = 0; + break; + case GSD_FONT_ANTIALIASING_MODE_GRAYSCALE: + settings->antialias = 1; + break; + case GSD_FONT_ANTIALIASING_MODE_RGBA: + settings->antialias = 1; + use_rgba = TRUE; + } + + if (!use_rgba) { + settings->rgba = "none"; + } +} + +static void +xft_settings_clear (GsdXftSettings *settings) +{ + g_free (settings->cursor_theme); +} + +static void +xft_settings_set_xsettings (GsdXSettingsManager *manager, + GsdXftSettings *settings) +{ + gnome_settings_profile_start (NULL); + + xsettings_manager_set_int (manager->manager, "Xft/Antialias", settings->antialias); + xsettings_manager_set_int (manager->manager, "Xft/Hinting", settings->hinting); + xsettings_manager_set_string (manager->manager, "Xft/HintStyle", settings->hintstyle); + xsettings_manager_set_int (manager->manager, "Gdk/WindowScalingFactor", settings->window_scale); + xsettings_manager_set_int (manager->manager, "Gdk/UnscaledDPI", settings->dpi); + xsettings_manager_set_int (manager->manager, "Xft/DPI", settings->scaled_dpi); + xsettings_manager_set_string (manager->manager, "Xft/RGBA", settings->rgba); + xsettings_manager_set_int (manager->manager, "Gtk/CursorThemeSize", settings->cursor_size); + xsettings_manager_set_string (manager->manager, "Gtk/CursorThemeName", settings->cursor_theme); + + gnome_settings_profile_end (NULL); +} + +static void +update_property (GString *props, const gchar* key, const gchar* value) +{ + gchar* needle; + size_t needle_len; + gchar* found = NULL; + + /* update an existing property */ + needle = g_strconcat (key, ":", NULL); + needle_len = strlen (needle); + if (g_str_has_prefix (props->str, needle)) + found = props->str; + else + found = strstr (props->str, needle); + + if (found) { + size_t value_index; + gchar* end; + + end = strchr (found, '\n'); + value_index = (found - props->str) + needle_len + 1; + g_string_erase (props, value_index, end ? (end - found - needle_len) : -1); + g_string_insert (props, value_index, "\n"); + g_string_insert (props, value_index, value); + } else { + g_string_append_printf (props, "%s:\t%s\n", key, value); + } + + g_free (needle); +} + +static void +xft_settings_set_xresources (GsdXftSettings *settings) +{ + GString *add_string; + char dpibuf[G_ASCII_DTOSTR_BUF_SIZE]; + Display *dpy; + + gnome_settings_profile_start (NULL); + + /* get existing properties */ + dpy = XOpenDisplay (NULL); + g_return_if_fail (dpy != NULL); + add_string = g_string_new (XResourceManagerString (dpy)); + + g_debug("xft_settings_set_xresources: orig res '%s'", add_string->str); + + g_snprintf (dpibuf, sizeof (dpibuf), "%d", (int) (settings->scaled_dpi / 1024.0 + 0.5)); + update_property (add_string, "Xft.dpi", dpibuf); + update_property (add_string, "Xft.antialias", + settings->antialias ? "1" : "0"); + update_property (add_string, "Xft.hinting", + settings->hinting ? "1" : "0"); + update_property (add_string, "Xft.hintstyle", + settings->hintstyle); + update_property (add_string, "Xft.rgba", + settings->rgba); + update_property (add_string, "Xcursor.size", + g_ascii_dtostr (dpibuf, sizeof (dpibuf), (double) settings->cursor_size)); + update_property (add_string, "Xcursor.theme", + settings->cursor_theme); + + g_debug("xft_settings_set_xresources: new res '%s'", add_string->str); + + /* Set the new X property */ + XChangeProperty(dpy, RootWindow (dpy, 0), + XA_RESOURCE_MANAGER, XA_STRING, 8, PropModeReplace, (const unsigned char *) add_string->str, add_string->len); + XCloseDisplay (dpy); + + g_string_free (add_string, TRUE); + + gnome_settings_profile_end (NULL); +} + +/* We mirror the Xft properties both through XSETTINGS and through + * X resources + */ +static void +update_xft_settings (GsdXSettingsManager *manager) +{ + GsdXftSettings settings; + + gnome_settings_profile_start (NULL); + + xft_settings_get (manager, &settings); + xft_settings_set_xsettings (manager, &settings); + xft_settings_set_xresources (&settings); + xft_settings_clear (&settings); + + gnome_settings_profile_end (NULL); +} + +static void +xft_callback (GSettings *settings, + const gchar *key, + GsdXSettingsManager *manager) +{ + update_xft_settings (manager); + queue_notify (manager); +} + +static void +override_callback (GSettings *settings, + const gchar *key, + GsdXSettingsManager *manager) +{ + GVariant *value; + + value = g_settings_get_value (settings, XSETTINGS_OVERRIDE_KEY); + + xsettings_manager_set_overrides (manager->manager, value); + queue_notify (manager); + + g_variant_unref (value); +} + +static void +plugin_callback (GSettings *settings, + const char *key, + GsdXSettingsManager *manager) +{ + if (g_str_equal (key, GTK_MODULES_DISABLED_KEY) || + g_str_equal (key, GTK_MODULES_ENABLED_KEY)) { + /* Do nothing, as GsdXsettingsGtk will handle it */ + } else if (g_str_equal (key, XSETTINGS_OVERRIDE_KEY)) { + override_callback (settings, key, manager); + } else { + xft_callback (settings, key, manager); + } +} + +static void +gtk_modules_callback (GsdXSettingsGtk *gtk, + GParamSpec *spec, + GsdXSettingsManager *manager) +{ + const char *modules = gsd_xsettings_gtk_get_modules (manager->gtk); + + if (modules == NULL) { + xsettings_manager_delete_setting (manager->manager, "Gtk/Modules"); + } else { + g_debug ("Setting GTK modules '%s'", modules); + xsettings_manager_set_string (manager->manager, + "Gtk/Modules", + modules); + } + + queue_notify (manager); + send_dbus_event (manager, GTK_SETTINGS_MODULES); +} + +static void +fontconfig_callback (FcMonitor *monitor, + GsdXSettingsManager *manager) +{ + gint64 timestamp = g_get_real_time (); + gint timestamp_sec = (int)(timestamp / G_TIME_SPAN_SECOND); + + gnome_settings_profile_start (NULL); + + xsettings_manager_set_int (manager->manager, "Fontconfig/Timestamp", timestamp_sec); + + manager->fontconfig_timestamp = timestamp; + + queue_notify (manager); + send_dbus_event (manager, GTK_SETTINGS_FONTCONFIG_TIMESTAMP); + gnome_settings_profile_end (NULL); +} + +static gboolean +start_fontconfig_monitor_idle_cb (GsdXSettingsManager *manager) +{ + gnome_settings_profile_start (NULL); + + fc_monitor_start (manager->fontconfig_monitor); + + gnome_settings_profile_end (NULL); + + manager->start_idle_id = 0; + + return FALSE; +} + +static void +start_fontconfig_monitor (GsdXSettingsManager *manager) +{ + gnome_settings_profile_start (NULL); + + manager->fontconfig_monitor = fc_monitor_new (); + g_signal_connect (manager->fontconfig_monitor, "updated", G_CALLBACK (fontconfig_callback), manager); + + manager->start_idle_id = g_idle_add ((GSourceFunc) start_fontconfig_monitor_idle_cb, manager); + g_source_set_name_by_id (manager->start_idle_id, "[gnome-settings-daemon] start_fontconfig_monitor_idle_cb"); + + gnome_settings_profile_end (NULL); +} + +static void +process_value (GsdXSettingsManager *manager, + TranslationEntry *trans, + GVariant *value) +{ + (* trans->translate) (manager, trans, value); +} + +static TranslationEntry * +find_translation_entry (GSettings *settings, const char *key) +{ + guint i; + char *schema; + + g_object_get (settings, "schema-id", &schema, NULL); + + if (g_str_equal (schema, CLASSIC_WM_SETTINGS_SCHEMA)) { + g_free (schema); + schema = g_strdup (WM_SETTINGS_SCHEMA); + } + + for (i = 0; i < G_N_ELEMENTS (translations); i++) { + if (g_str_equal (schema, translations[i].gsettings_schema) && + g_str_equal (key, translations[i].gsettings_key)) { + g_free (schema); + return &translations[i]; + } + } + + g_free (schema); + + return NULL; +} + +static void +xsettings_callback (GSettings *settings, + const char *key, + GsdXSettingsManager *manager) +{ + TranslationEntry *trans; + GVariant *value; + + if (g_str_equal (key, TEXT_SCALING_FACTOR_KEY) || + g_str_equal (key, CURSOR_SIZE_KEY) || + g_str_equal (key, CURSOR_THEME_KEY)) { + xft_callback (NULL, key, manager); + return; + } + + trans = find_translation_entry (settings, key); + if (trans == NULL) { + return; + } + + value = g_settings_get_value (settings, key); + + process_value (manager, trans, value); + + g_variant_unref (value); + + queue_notify (manager); +} + +static void +terminate_cb (void *data) +{ + gboolean *terminated = data; + + if (*terminated) { + return; + } + + *terminated = TRUE; + g_warning ("X Settings Manager is terminating"); + gtk_main_quit (); +} + +static gboolean +setup_xsettings_managers (GsdXSettingsManager *manager) +{ + GdkDisplay *display; + gboolean res; + gboolean terminated; + + display = gdk_display_get_default (); + + res = xsettings_manager_check_running (gdk_x11_display_get_xdisplay (display), + gdk_x11_screen_get_screen_number (gdk_screen_get_default ())); + + if (res) { + g_warning ("You can only run one xsettings manager at a time; exiting"); + return FALSE; + } + + terminated = FALSE; + manager->manager = xsettings_manager_new (gdk_x11_display_get_xdisplay (display), + gdk_x11_screen_get_screen_number (gdk_screen_get_default ()), + terminate_cb, + &terminated); + if (! manager->manager) { + g_warning ("Could not create xsettings manager!"); + return FALSE; + } + + return TRUE; +} + +static void +monitors_changed (GsdXSettingsManager *manager) +{ + update_xft_settings (manager); + queue_notify (manager); +} + +static void +on_monitors_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer data) +{ + GsdXSettingsManager *manager = data; + monitors_changed (manager); +} + +static void +on_display_config_name_appeared_handler (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer data) +{ + GsdXSettingsManager *manager = data; + monitors_changed (manager); +} + +static void +animations_enabled_changed (GsdXSettingsManager *manager) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) res = NULL; + g_autoptr(GVariant) animations_enabled_variant = NULL; + gboolean animations_enabled; + + res = g_dbus_connection_call_sync (manager->dbus_connection, + "org.gnome.Shell.Introspect", + "/org/gnome/Shell/Introspect", + "org.freedesktop.DBus.Properties", + "Get", + g_variant_new ("(ss)", + "org.gnome.Shell.Introspect", + "AnimationsEnabled"), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &error); + if (!res) { + g_warning ("Failed to get animations-enabled state: %s", + error->message); + return; + } + + g_variant_get (res, "(v)", &animations_enabled_variant); + g_variant_get (animations_enabled_variant, "b", &animations_enabled); + + if (manager->enable_animations == animations_enabled) + return; + + manager->enable_animations = animations_enabled; + xsettings_manager_set_int (manager->manager, "Gtk/EnableAnimations", + animations_enabled); + queue_notify (manager); + send_dbus_event (manager, GTK_SETTINGS_ENABLE_ANIMATIONS); +} + +static void +on_introspect_properties_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer data) +{ + GsdXSettingsManager *manager = data; + animations_enabled_changed (manager); +} + +static void +on_shell_introspect_name_appeared_handler (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer data) +{ + GsdXSettingsManager *manager = data; + animations_enabled_changed (manager); +} + +static void +launch_xwayland_services_on_dir (const gchar *path) +{ + GFileEnumerator *enumerator; + GError *error = NULL; + GList *l, *scripts = NULL; + GFile *dir; + + g_debug ("launch_xwayland_services_on_dir: %s", path); + + dir = g_file_new_for_path (path); + enumerator = g_file_enumerate_children (dir, + G_FILE_ATTRIBUTE_STANDARD_NAME "," + G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE "," + G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NONE, + NULL, &error); + g_object_unref (dir); + + if (!enumerator) { + if (!g_error_matches (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND)) { + g_warning ("Error opening '%s': %s", + path, error->message); + } + + g_error_free (error); + return; + } + + while (TRUE) { + GFileInfo *info; + GFile *child; + + if (!g_file_enumerator_iterate (enumerator, + &info, &child, + NULL, &error)) { + g_warning ("Error iterating on '%s': %s", + path, error->message); + g_error_free (error); + break; + } + + if (!info) + break; + + if (g_file_info_get_file_type (info) != G_FILE_TYPE_REGULAR || + !g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE)) + continue; + + scripts = g_list_prepend (scripts, g_file_get_path (child)); + } + + scripts = g_list_sort (scripts, (GCompareFunc) strcmp); + + for (l = scripts; l; l = l->next) { + gchar *args[2] = { l->data, NULL }; + + g_debug ("launch_xwayland_services_on_dir: Spawning '%s'", args[0]); + if (!g_spawn_sync (NULL, args, NULL, + G_SPAWN_DEFAULT, + NULL, NULL, + NULL, NULL, NULL, + &error)) { + g_warning ("Error when spawning '%s': %s", + args[0], error->message); + g_clear_error (&error); + } + } + + g_object_unref (enumerator); + g_list_free_full (scripts, g_free); +} + +static void +launch_xwayland_services (void) +{ + const gchar * const * config_dirs; + gint i; + + config_dirs = g_get_system_config_dirs (); + + for (i = 0; config_dirs[i] != NULL; i++) { + gchar *config_dir; + + config_dir = g_build_filename (config_dirs[i], + "Xwayland-session.d", + NULL); + + launch_xwayland_services_on_dir (config_dir); + g_free (config_dir); + } +} + +gboolean +gsd_xsettings_manager_start (GsdXSettingsManager *manager, + GError **error) +{ + GVariant *overrides; + guint i; + GList *list, *l; + const char *session; + + g_debug ("Starting xsettings manager"); + gnome_settings_profile_start (NULL); + + if (!setup_xsettings_managers (manager)) { + g_set_error (error, GSD_XSETTINGS_ERROR, + GSD_XSETTINGS_ERROR_INIT, + "Could not initialize xsettings manager."); + return FALSE; + } + + manager->monitors_changed_id = + g_dbus_connection_signal_subscribe (manager->dbus_connection, + "org.gnome.Mutter.DisplayConfig", + "org.gnome.Mutter.DisplayConfig", + "MonitorsChanged", + "/org/gnome/Mutter/DisplayConfig", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + on_monitors_changed, + manager, + NULL); + manager->display_config_watch_id = + g_bus_watch_name_on_connection (manager->dbus_connection, + "org.gnome.Mutter.DisplayConfig", + G_BUS_NAME_WATCHER_FLAGS_NONE, + on_display_config_name_appeared_handler, + NULL, + manager, + NULL); + + manager->introspect_properties_changed_id = + g_dbus_connection_signal_subscribe (manager->dbus_connection, + "org.gnome.Shell.Introspect", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + "/org/gnome/Shell/Introspect", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + on_introspect_properties_changed, + manager, + NULL); + manager->shell_introspect_watch_id = + g_bus_watch_name_on_connection (manager->dbus_connection, + "org.gnome.Shell.Introspect", + G_BUS_NAME_WATCHER_FLAGS_NONE, + on_shell_introspect_name_appeared_handler, + NULL, + manager, + NULL); + + manager->settings = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, (GDestroyNotify) g_object_unref); + + g_hash_table_insert (manager->settings, + MOUSE_SETTINGS_SCHEMA, g_settings_new (MOUSE_SETTINGS_SCHEMA)); + g_hash_table_insert (manager->settings, + BACKGROUND_SETTINGS_SCHEMA, g_settings_new (BACKGROUND_SETTINGS_SCHEMA)); + g_hash_table_insert (manager->settings, + INTERFACE_SETTINGS_SCHEMA, g_settings_new (INTERFACE_SETTINGS_SCHEMA)); + g_hash_table_insert (manager->settings, + SOUND_SETTINGS_SCHEMA, g_settings_new (SOUND_SETTINGS_SCHEMA)); + g_hash_table_insert (manager->settings, + PRIVACY_SETTINGS_SCHEMA, g_settings_new (PRIVACY_SETTINGS_SCHEMA)); + g_hash_table_insert (manager->settings, + WM_SETTINGS_SCHEMA, g_settings_new (WM_SETTINGS_SCHEMA)); + g_hash_table_insert (manager->settings, + A11Y_SCHEMA, g_settings_new (A11Y_SCHEMA)); + + session = g_getenv ("XDG_CURRENT_DESKTOP"); + if (session && strstr (session, "GNOME-Classic")) { + GSettingsSchema *schema; + + schema = g_settings_schema_source_lookup (g_settings_schema_source_get_default (), + CLASSIC_WM_SETTINGS_SCHEMA, FALSE); + if (schema) { + g_hash_table_insert (manager->settings, + CLASSIC_WM_SETTINGS_SCHEMA, + g_settings_new_full (schema, NULL, NULL)); + g_settings_schema_unref (schema); + } + } + + for (i = 0; i < G_N_ELEMENTS (fixed_entries); i++) { + FixedEntry *fixed = &fixed_entries[i]; + (* fixed->func) (manager, fixed); + } + + list = g_hash_table_get_values (manager->settings); + for (l = list; l != NULL; l = l->next) { + g_signal_connect_object (G_OBJECT (l->data), "changed", G_CALLBACK (xsettings_callback), manager, 0); + } + g_list_free (list); + + for (i = 0; i < G_N_ELEMENTS (translations); i++) { + GVariant *val; + GSettings *settings; + + settings = g_hash_table_lookup (manager->settings, + translations[i].gsettings_schema); + if (settings == NULL) { + g_warning ("Schemas '%s' has not been setup", translations[i].gsettings_schema); + continue; + } + + val = g_settings_get_value (settings, translations[i].gsettings_key); + + process_value (manager, &translations[i], val); + g_variant_unref (val); + } + + /* Plugin settings (GTK modules and Xft) */ + manager->plugin_settings = g_settings_new (XSETTINGS_PLUGIN_SCHEMA); + g_signal_connect_object (manager->plugin_settings, "changed", G_CALLBACK (plugin_callback), manager, 0); + + manager->gtk = gsd_xsettings_gtk_new (); + g_signal_connect (G_OBJECT (manager->gtk), "notify::gtk-modules", + G_CALLBACK (gtk_modules_callback), manager); + gtk_modules_callback (manager->gtk, NULL, manager); + + /* Xft settings */ + update_xft_settings (manager); + + /* Launch Xwayland services */ + if (gnome_settings_is_wayland ()) + launch_xwayland_services (); + + register_manager_dbus (manager); + + start_fontconfig_monitor (manager); + + overrides = g_settings_get_value (manager->plugin_settings, XSETTINGS_OVERRIDE_KEY); + xsettings_manager_set_overrides (manager->manager, overrides); + queue_notify (manager); + g_variant_unref (overrides); + + + gnome_settings_profile_end (NULL); + + return TRUE; +} + +void +gsd_xsettings_manager_stop (GsdXSettingsManager *manager) +{ + g_debug ("Stopping xsettings manager"); + + if (manager->introspect_properties_changed_id) { + g_dbus_connection_signal_unsubscribe (manager->dbus_connection, + manager->introspect_properties_changed_id); + manager->introspect_properties_changed_id = 0; + } + + if (manager->shell_introspect_watch_id) { + g_bus_unwatch_name (manager->shell_introspect_watch_id); + manager->shell_introspect_watch_id = 0; + } + + if (manager->monitors_changed_id) { + g_dbus_connection_signal_unsubscribe (manager->dbus_connection, + manager->monitors_changed_id); + manager->monitors_changed_id = 0; + } + + if (manager->display_config_watch_id) { + g_bus_unwatch_name (manager->display_config_watch_id); + manager->display_config_watch_id = 0; + } + + if (manager->shell_name_watch_id > 0) { + g_bus_unwatch_name (manager->shell_name_watch_id); + manager->shell_name_watch_id = 0; + } + + if (manager->manager != NULL) { + xsettings_manager_destroy (manager->manager); + manager->manager = NULL; + } + + if (manager->plugin_settings != NULL) { + g_signal_handlers_disconnect_by_data (manager->plugin_settings, manager); + g_object_unref (manager->plugin_settings); + manager->plugin_settings = NULL; + } + + if (manager->gtk_settings_name_id > 0) { + g_bus_unown_name (manager->gtk_settings_name_id); + manager->gtk_settings_name_id = 0; + } + + if (manager->fontconfig_monitor != NULL) { + g_signal_handlers_disconnect_by_data (manager->fontconfig_monitor, manager); + fc_monitor_stop (manager->fontconfig_monitor); + g_object_unref (manager->fontconfig_monitor); + manager->fontconfig_monitor = NULL; + } + + if (manager->settings != NULL) { + g_hash_table_destroy (manager->settings); + manager->settings = NULL; + } + + if (manager->gtk != NULL) { + g_object_unref (manager->gtk); + manager->gtk = NULL; + } +} + +static void +gsd_xsettings_manager_class_init (GsdXSettingsManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsd_xsettings_manager_finalize; +} + +static void +gsd_xsettings_manager_init (GsdXSettingsManager *manager) +{ + GError *error = NULL; + + manager->dbus_connection = g_bus_get_sync (G_BUS_TYPE_SESSION, + NULL, &error); + if (!manager->dbus_connection) + g_error ("Failed to get session bus: %s", error->message); +} + +static void +gsd_xsettings_manager_finalize (GObject *object) +{ + GsdXSettingsManager *xsettings_manager; + + g_return_if_fail (object != NULL); + g_return_if_fail (GSD_IS_XSETTINGS_MANAGER (object)); + + xsettings_manager = GSD_XSETTINGS_MANAGER (object); + + g_return_if_fail (xsettings_manager != NULL); + + gsd_xsettings_manager_stop (xsettings_manager); + + if (xsettings_manager->start_idle_id != 0) + g_source_remove (xsettings_manager->start_idle_id); + + g_clear_object (&xsettings_manager->dbus_connection); + + G_OBJECT_CLASS (gsd_xsettings_manager_parent_class)->finalize (object); +} + +static GVariant * +handle_get_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, + gpointer user_data) +{ + GsdXSettingsManager *manager = user_data; + + if (g_strcmp0 (property_name, "FontconfigTimestamp") == 0) { + return g_variant_new_int64 (manager->fontconfig_timestamp); + } else if (g_strcmp0 (property_name, "Modules") == 0) { + const char *modules = gsd_xsettings_gtk_get_modules (manager->gtk); + return g_variant_new_string (modules ? modules : ""); + } else if (g_strcmp0 (property_name, "EnableAnimations") == 0) { + return g_variant_new_boolean (manager->enable_animations); + } else { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "No such interface: %s", interface_name); + return NULL; + } +} + +static const GDBusInterfaceVTable interface_vtable = +{ + NULL, + handle_get_property, + NULL +}; + +static void +register_manager_dbus (GsdXSettingsManager *manager) +{ + g_assert (manager->dbus_connection != NULL); + + manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + g_assert (manager->introspection_data != NULL); + + g_dbus_connection_register_object (manager->dbus_connection, + GTK_SETTINGS_DBUS_PATH, + manager->introspection_data->interfaces[0], + &interface_vtable, + manager, + NULL, + NULL); + + manager->gtk_settings_name_id = g_bus_own_name_on_connection (manager->dbus_connection, + GTK_SETTINGS_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, NULL, NULL, NULL); +} + +GsdXSettingsManager * +gsd_xsettings_manager_new (void) +{ + if (manager_object != NULL) { + g_object_ref (manager_object); + } else { + manager_object = g_object_new (GSD_TYPE_XSETTINGS_MANAGER, NULL); + g_object_add_weak_pointer (manager_object, + (gpointer *) &manager_object); + } + + return GSD_XSETTINGS_MANAGER (manager_object); +} diff --git a/plugins/xsettings/gsd-xsettings-manager.h b/plugins/xsettings/gsd-xsettings-manager.h new file mode 100644 index 0000000..96fff13 --- /dev/null +++ b/plugins/xsettings/gsd-xsettings-manager.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __GSD_XSETTINGS_MANAGER_H +#define __GSD_XSETTINGS_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GSD_TYPE_XSETTINGS_MANAGER (gsd_xsettings_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (GsdXSettingsManager, gsd_xsettings_manager, GSD, XSETTINGS_MANAGER, GObject) + +GsdXSettingsManager * gsd_xsettings_manager_new (void); +gboolean gsd_xsettings_manager_start (GsdXSettingsManager *manager, + GError **error); +void gsd_xsettings_manager_stop (GsdXSettingsManager *manager); + +G_END_DECLS + +#endif /* __GNOME_XSETTINGS_MANAGER_H */ diff --git a/plugins/xsettings/gtk-modules-test/canberra-gtk-module.desktop b/plugins/xsettings/gtk-modules-test/canberra-gtk-module.desktop new file mode 100644 index 0000000..80a087b --- /dev/null +++ b/plugins/xsettings/gtk-modules-test/canberra-gtk-module.desktop @@ -0,0 +1,6 @@ +[GTK Module] +Name=canberra-gtk-module +Description=Event Sound Module +X-GTK-Module-Name=canberra-gtk-module +X-GTK-Module-Enabled-Schema=org.gnome.desktop.sound +X-GTK-Module-Enabled-Key=event-sounds diff --git a/plugins/xsettings/gtk-modules-test/pk-gtk-module.desktop b/plugins/xsettings/gtk-modules-test/pk-gtk-module.desktop new file mode 100644 index 0000000..f13cf0f --- /dev/null +++ b/plugins/xsettings/gtk-modules-test/pk-gtk-module.desktop @@ -0,0 +1,4 @@ +[GTK Module] +Name=PackageKit +Description=PackageKit Font Installer +X-GTK-Module-Name=pk-gtk-module diff --git a/plugins/xsettings/main.c b/plugins/xsettings/main.c new file mode 100644 index 0000000..65d9a20 --- /dev/null +++ b/plugins/xsettings/main.c @@ -0,0 +1,8 @@ +#define NEW gsd_xsettings_manager_new +#define START gsd_xsettings_manager_start +#define STOP gsd_xsettings_manager_stop +#define MANAGER GsdXSettingsManager +#define GDK_BACKEND "x11" +#include "gsd-xsettings-manager.h" + +#include "daemon-skeleton-gtk.h" diff --git a/plugins/xsettings/meson.build b/plugins/xsettings/meson.build new file mode 100644 index 0000000..f91b963 --- /dev/null +++ b/plugins/xsettings/meson.build @@ -0,0 +1,66 @@ +gsd_xsettings_gtk = files('gsd-xsettings-gtk.c') + +fc_monitor = files('fc-monitor.c') + +wm_button_layout_translation = files('wm-button-layout-translation.c') + +sources = gsd_xsettings_gtk + fc_monitor + wm_button_layout_translation + files( + 'gsd-xsettings-manager.c', + 'xsettings-common.c', + 'xsettings-manager.c', + 'main.c' +) + +deps = plugins_deps + [ + gtk_dep, + x11_dep, + dependency('fontconfig') +] + +cflags += ['-DGTK_MODULES_DIRECTORY="@0@"'.format(join_paths(gsd_pkglibdir, 'gtk-modules'))] + +executable( + 'gsd-' + plugin_name, + sources, + include_directories: [top_inc, common_inc, data_inc], + dependencies: deps, + c_args: cflags, + install: true, + install_rpath: gsd_pkglibdir, + install_dir: gsd_libexecdir +) + +programs = [ + ['test-gtk-modules', gsd_xsettings_gtk + ['test-gtk-modules.c'], cflags], + ['test-fontconfig-monitor', fc_monitor, cflags + ['-DFONTCONFIG_MONITOR_TEST']], + ['test-wm-button-layout-translations', wm_button_layout_translation + ['test-wm-button-layout-translations.c'], []] +] + +foreach program: programs + executable( + program[0], + program[1], + include_directories: top_inc, + dependencies: deps, + c_args: program[2] + ) +endforeach + +install_data( + files('00-xrdb'), + install_dir: join_paths(gsd_sysconfdir, 'xdg/Xwayland-session.d') +) + +test_py = find_program('test.py') + +envs = [ + 'BUILDDIR=' + meson.current_build_dir(), + 'TOP_BUILDDIR=' + meson.build_root() +] + +test( + 'test-xsettings', + test_py, + env: envs, + timeout: 300 +) diff --git a/plugins/xsettings/test-gtk-modules.c b/plugins/xsettings/test-gtk-modules.c new file mode 100644 index 0000000..ef83fc3 --- /dev/null +++ b/plugins/xsettings/test-gtk-modules.c @@ -0,0 +1,31 @@ + + +#include "gsd-xsettings-gtk.h" + +static void +gtk_modules_callback (GsdXSettingsGtk *gtk, + GParamSpec *spec, + gpointer user_data) +{ + const char *modules; + + modules = gsd_xsettings_gtk_get_modules (gtk); + g_message ("GTK+ modules list changed to: %s", modules ? modules : "(empty)"); +} + +int main (int argc, char **argv) +{ + GMainLoop *loop; + GsdXSettingsGtk *gtk; + + gtk = gsd_xsettings_gtk_new (); + g_signal_connect (G_OBJECT (gtk), "notify::gtk-modules", + G_CALLBACK (gtk_modules_callback), NULL); + + gtk_modules_callback (gtk, NULL, NULL); + + loop = g_main_loop_new (NULL, TRUE); + g_main_loop_run (loop); + + return 0; +} diff --git a/plugins/xsettings/test-wm-button-layout-translations.c b/plugins/xsettings/test-wm-button-layout-translations.c new file mode 100644 index 0000000..5ab140a --- /dev/null +++ b/plugins/xsettings/test-wm-button-layout-translations.c @@ -0,0 +1,54 @@ +#include <glib.h> + +#include "wm-button-layout-translation.h" + +static void +test_button_layout_translations (void) +{ + static struct { + char *layout; + char *expected; + } tests[] = { + { "", "" }, + { "invalid", "" }, + + { ":", ":" }, + { ":invalid", ":" }, + { "invalid:", ":" }, + { "invalid:invalid", ":" }, + + { "appmenu", "menu" }, + { "appmenu:", "menu:" }, + { ":menu", ":icon" }, + { "appmenu:close", "menu:close" }, + { "appmenu:minimize,maximize,close", "menu:minimize,maximize,close" }, + { "menu,appmenu:minimize,maximize,close", "icon,menu:minimize,maximize,close" }, + + { "close,close,close:close,close,close", "close,close,close:close,close,close" }, + + { "invalid,appmenu:invalid,minimize", "menu:minimize" }, + { "appmenu,invalid:minimize,invalid", "menu:minimize" }, + { "invalidmenu:invalidclose", ":" }, + { "invalid,invalid,invalid:invalid,minimize,maximize,close", ":minimize,maximize,close" }, + }; + int i; + + for (i = 0; i < G_N_ELEMENTS (tests); i++) + { + char *layout = g_strdup (tests[i].layout); + + translate_wm_button_layout_to_gtk (layout); + g_assert_cmpstr (layout, ==, tests[i].expected); + g_free (layout); + } +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/layout-translations", test_button_layout_translations); + + return g_test_run (); +} diff --git a/plugins/xsettings/test.py b/plugins/xsettings/test.py new file mode 100755 index 0000000..0b3a44d --- /dev/null +++ b/plugins/xsettings/test.py @@ -0,0 +1,209 @@ +#!/usr/bin/python3 +'''GNOME settings daemon tests for xsettings plugin.''' + +__author__ = 'Bastien Nocera <hadess@hadess.net>' +__copyright__ = '(C) 2018 Red Hat, Inc.' +__license__ = 'GPL v2 or later' + +import unittest +import subprocess +import sys +import time +import os +import os.path +import signal +import shutil + +project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +builddir = os.environ.get('BUILDDIR', os.path.dirname(__file__)) + +sys.path.insert(0, os.path.join(project_root, 'tests')) +sys.path.insert(0, builddir) +import gsdtestcase +import dbus +import dbusmock + +from gi.repository import Gio +from gi.repository import GLib + +class XsettingsPluginTest(gsdtestcase.GSDTestCase): + '''Test the xsettings plugin''' + + def setUp(self): + self.start_logind() + + self.daemon_death_expected = False + self.session_log_write = open(os.path.join(self.workdir, 'gnome-session.log'), 'wb') + self.session = subprocess.Popen(['gnome-session', '-f', + '-a', os.path.join(self.workdir, 'autostart'), + '--session=dummy', '--debug'], + stdout=self.session_log_write, + stderr=subprocess.STDOUT) + + # wait until the daemon is on the bus + try: + self.wait_for_bus_object('org.gnome.SessionManager', + '/org/gnome/SessionManager') + except: + # on failure, print log + with open(self.session_log_write.name) as f: + print('----- session log -----\n%s\n------' % f.read()) + raise + + self.session_log = open(self.session_log_write.name) + + self.obj_session_mgr = self.session_bus_con.get_object( + 'org.gnome.SessionManager', '/org/gnome/SessionManager') + + self.start_mutter() + + Gio.Settings.sync() + self.plugin_log_write = open(os.path.join(self.workdir, 'plugin_xsettings.log'), 'wb', buffering=0) + os.environ['GSD_ignore_llvmpipe'] = '1' + + # Setup fontconfig config path before starting the daemon + self.fc_dir = os.path.join(self.workdir, 'fontconfig') + os.environ['FONTCONFIG_PATH'] = self.fc_dir + try: + os.makedirs(self.fc_dir) + except: + pass + shutil.copy(os.path.join(os.path.dirname(__file__), 'fontconfig-test/fonts.conf'), + os.path.join(self.fc_dir, 'fonts.conf')) + + # Setup GTK+ modules before starting the daemon + modules_dir = os.path.join(self.workdir, 'gtk-modules') + os.environ['GSD_gtk_modules_dir'] = modules_dir + try: + os.makedirs(modules_dir) + except: + pass + shutil.copy(os.path.join(os.path.dirname(__file__), 'gtk-modules-test/canberra-gtk-module.desktop'), + os.path.join(modules_dir, 'canberra-gtk-module.desktop')) + shutil.copy(os.path.join(os.path.dirname(__file__), 'gtk-modules-test/pk-gtk-module.desktop'), + os.path.join(modules_dir, 'pk-gtk-module.desktop')) + + self.settings_sound = Gio.Settings.new('org.gnome.desktop.sound') + + env = os.environ.copy() + self.daemon = subprocess.Popen( + [os.path.join(builddir, 'gsd-xsettings'), '--verbose'], + # comment out this line if you want to see the logs in real time + stdout=self.plugin_log_write, + stderr=subprocess.STDOUT, + env=env) + + # you can use this for reading the current daemon log in tests + self.plugin_log = open(self.plugin_log_write.name, 'rb', buffering=0) + + # flush notification log + try: + self.p_notify.stdout.read() + except IOError: + pass + + time.sleep(3) + obj_xsettings = self.session_bus_con.get_object( + 'org.gtk.Settings', '/org/gtk/Settings') + self.obj_xsettings_props = dbus.Interface(obj_xsettings, dbus.PROPERTIES_IFACE) + + def tearDown(self): + + daemon_running = self.daemon.poll() == None + if daemon_running: + self.daemon.terminate() + self.daemon.wait() + self.plugin_log.close() + self.plugin_log_write.flush() + self.plugin_log_write.close() + + self.stop_session() + self.stop_mutter() + self.stop_logind() + + # reset all changed gsettings, so that tests are independent from each + # other + for schema in [self.settings_sound]: + for k in schema.list_keys(): + schema.reset(k) + Gio.Settings.sync() + + # we check this at the end so that the other cleanup always happens + self.assertTrue(daemon_running or self.daemon_death_expected, 'daemon died during the test') + + def stop_session(self): + '''Stop GNOME session''' + + assert self.session + self.session.terminate() + self.session.wait() + + self.session_log_write.flush() + self.session_log_write.close() + self.session_log.close() + + def check_plugin_log(self, needle, timeout=0, failmsg=None): + '''Check that needle is found in the log within the given timeout. + Returns immediately when found. + + Fail after the given timeout. + ''' + if type(needle) == str: + needle = needle.encode('ascii') + # Fast path if the message was already logged + log = self.plugin_log.read() + if needle in log: + return + + while timeout > 0: + time.sleep(0.5) + timeout -= 0.5 + + # read new data (lines) from the log + log = self.plugin_log.read() + if needle in log: + break + else: + if failmsg is not None: + self.fail(failmsg) + else: + self.fail('timed out waiting for needle "%s"' % needle) + + def test_gtk_modules(self): + # Turn off event sounds + self.settings_sound['event-sounds'] = False + time.sleep(2) + + # Verify that only the PackageKit plugin is enabled + self.assertEqual(self.obj_xsettings_props.Get('org.gtk.Settings', 'Modules'), + dbus.String('pk-gtk-module', variant_level=1)) + + # Turn on sounds + self.settings_sound['event-sounds'] = True + time.sleep(2) + + # Check that both PK and canberra plugin are enabled + retval = self.obj_xsettings_props.Get('org.gtk.Settings', 'Modules') + values = sorted(str(retval).split(':')) + self.assertEqual(values, ['canberra-gtk-module', 'pk-gtk-module']) + + def test_fontconfig_timestamp(self): + # Initially, the value is zero + before = self.obj_xsettings_props.Get('org.gtk.Settings', 'FontconfigTimestamp') + self.assertEqual(before, 0) + + # Copy the fonts.conf again + shutil.copy(os.path.join(os.path.dirname(__file__), 'fontconfig-test/fonts.conf'), + os.path.join(self.fc_dir, 'fonts.conf')) + + # Wait for gsd-xsettings to pick up the change (and process it) + self.check_plugin_log("Fontconfig update successful", timeout=5, failmsg="Fontconfig was not updated!") + + # Sleep a bit to ensure that the setting is updated + time.sleep(1) + + after = self.obj_xsettings_props.Get('org.gtk.Settings', 'FontconfigTimestamp') + self.assertTrue(after > before) + +# avoid writing to stderr +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/plugins/xsettings/wm-button-layout-translation.c b/plugins/xsettings/wm-button-layout-translation.c new file mode 100644 index 0000000..0fa4d2c --- /dev/null +++ b/plugins/xsettings/wm-button-layout-translation.c @@ -0,0 +1,88 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2014 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Author: Florian Müllner <fmuellner@gnome.org> + */ + +#include <stdio.h> +#include <string.h> +#include <glib.h> + +#include "wm-button-layout-translation.h" + +static void +translate_buttons (char *layout, int *len_p) +{ + char *strp = layout, *button; + int len = 0; + + if (!layout || !*layout) + goto out; + + while ((button = strsep (&strp, ","))) + { + char *gtkbutton; + + if (strcmp (button, "menu") == 0) + gtkbutton = "icon"; + else if (strcmp (button, "appmenu") == 0) + gtkbutton = "menu"; + else if (strcmp (button, "minimize") == 0) + gtkbutton = "minimize"; + else if (strcmp (button, "maximize") == 0) + gtkbutton = "maximize"; + else if (strcmp (button, "close") == 0) + gtkbutton = "close"; + else + continue; + + if (len) + layout[len++] = ','; + + strcpy (layout + len, gtkbutton); + len += strlen (gtkbutton); + } + layout[len] = '\0'; + +out: + if (len_p) + *len_p = len; +} + +void +translate_wm_button_layout_to_gtk (char *layout) +{ + char *strp = layout, *left_buttons, *right_buttons; + int left_len, right_len = 0; + + left_buttons = strsep (&strp, ":"); + right_buttons = strp; + + translate_buttons (left_buttons, &left_len); + memmove (layout, left_buttons, left_len); + + if (strp == NULL) + goto out; /* no ":" in layout */ + + layout[left_len++] = ':'; + + translate_buttons (right_buttons, &right_len); + memmove (layout + left_len, right_buttons, right_len); + +out: + layout[left_len + right_len] = '\0'; +} diff --git a/plugins/xsettings/wm-button-layout-translation.h b/plugins/xsettings/wm-button-layout-translation.h new file mode 100644 index 0000000..87210b6 --- /dev/null +++ b/plugins/xsettings/wm-button-layout-translation.h @@ -0,0 +1,26 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2014 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Author: Florian Müllner <fmuellner@gnome.org> + */ + +#ifndef __WM_BUTTON_LAYOUT_TRANSLATION__ +#define __WM_BUTTON_LAYOUT_TRANSLATION__ + +void translate_wm_button_layout_to_gtk (char *layout); + +#endif diff --git a/plugins/xsettings/xsettings-common.c b/plugins/xsettings/xsettings-common.c new file mode 100644 index 0000000..e5f9fcc --- /dev/null +++ b/plugins/xsettings/xsettings-common.c @@ -0,0 +1,112 @@ +/* + * Copyright © 2001 Red Hat, Inc. + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation, and that the name of Red Hat not be used in advertising or + * publicity pertaining to distribution of the software without specific, + * written prior permission. Red Hat makes no representations about the + * suitability of this software for any purpose. It is provided "as is" + * without express or implied warranty. + * + * RED HAT DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL RED HAT + * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Author: Owen Taylor, Red Hat, Inc. + */ + +#include <glib.h> + +#include "string.h" +#include "stdlib.h" + +#include <X11/Xlib.h> +#include <X11/Xmd.h> /* For CARD32 */ + +#include "xsettings-common.h" + +XSettingsSetting * +xsettings_setting_new (const gchar *name) +{ + XSettingsSetting *result; + + result = g_slice_new0 (XSettingsSetting); + result->name = g_strdup (name); + + return result; +} + +static gboolean +xsettings_variant_equal0 (GVariant *a, + GVariant *b) +{ + if (a == b) + return TRUE; + + if (!a || !b) + return FALSE; + + return g_variant_equal (a, b); +} + +GVariant * +xsettings_setting_get (XSettingsSetting *setting) +{ + gint i; + + for (i = G_N_ELEMENTS (setting->value) - 1; 0 <= i; i--) + if (setting->value[i]) + return setting->value[i]; + + return NULL; +} + +void +xsettings_setting_set (XSettingsSetting *setting, + gint tier, + GVariant *value, + guint32 serial) +{ + GVariant *old_value; + + old_value = xsettings_setting_get (setting); + if (old_value) + g_variant_ref (old_value); + + if (setting->value[tier]) + g_variant_unref (setting->value[tier]); + setting->value[tier] = value ? g_variant_ref_sink (value) : NULL; + + if (!xsettings_variant_equal0 (old_value, xsettings_setting_get (setting))) + setting->last_change_serial = serial; + + if (old_value) + g_variant_unref (old_value); +} + +void +xsettings_setting_free (XSettingsSetting *setting) +{ + gint i; + + for (i = 0; i < G_N_ELEMENTS (setting->value); i++) + if (setting->value[i]) + g_variant_unref (setting->value[i]); + + g_free (setting->name); + + g_slice_free (XSettingsSetting, setting); +} + +char +xsettings_byte_order (void) +{ + CARD32 myint = 0x01020304; + return (*(char *)&myint == 1) ? MSBFirst : LSBFirst; +} diff --git a/plugins/xsettings/xsettings-common.h b/plugins/xsettings/xsettings-common.h new file mode 100644 index 0000000..2062f47 --- /dev/null +++ b/plugins/xsettings/xsettings-common.h @@ -0,0 +1,65 @@ +/* + * Copyright © 2001 Red Hat, Inc. + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation, and that the name of Red Hat not be used in advertising or + * publicity pertaining to distribution of the software without specific, + * written prior permission. Red Hat makes no representations about the + * suitability of this software for any purpose. It is provided "as is" + * without express or implied warranty. + * + * RED HAT DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL RED HAT + * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Author: Owen Taylor, Red Hat, Inc. + */ +#ifndef XSETTINGS_COMMON_H +#define XSETTINGS_COMMON_H + +#include <glib.h> + +#define XSETTINGS_N_TIERS 2 + +typedef struct _XSettingsColor XSettingsColor; +typedef struct _XSettingsSetting XSettingsSetting; + +/* Types of settings possible. Enum values correspond to + * protocol values. + */ +typedef enum +{ + XSETTINGS_TYPE_INT = 0, + XSETTINGS_TYPE_STRING = 1, + XSETTINGS_TYPE_COLOR = 2 +} XSettingsType; + +struct _XSettingsColor +{ + unsigned short red, green, blue, alpha; +}; + +struct _XSettingsSetting +{ + char *name; + GVariant *value[XSETTINGS_N_TIERS]; + unsigned long last_change_serial; +}; + +XSettingsSetting *xsettings_setting_new (const gchar *name); +GVariant * xsettings_setting_get (XSettingsSetting *setting); +void xsettings_setting_set (XSettingsSetting *setting, + gint tier, + GVariant *value, + guint32 serial); +void xsettings_setting_free (XSettingsSetting *setting); + +char xsettings_byte_order (void); + +#endif /* XSETTINGS_COMMON_H */ diff --git a/plugins/xsettings/xsettings-manager.c b/plugins/xsettings/xsettings-manager.c new file mode 100644 index 0000000..947cc9e --- /dev/null +++ b/plugins/xsettings/xsettings-manager.c @@ -0,0 +1,393 @@ +/* + * Copyright © 2001 Red Hat, Inc. + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation, and that the name of Red Hat not be used in advertising or + * publicity pertaining to distribution of the software without specific, + * written prior permission. Red Hat makes no representations about the + * suitability of this software for any purpose. It is provided "as is" + * without express or implied warranty. + * + * RED HAT DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL RED HAT + * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Author: Owen Taylor, Red Hat, Inc. + */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <glib.h> +#include <X11/Xmd.h> /* For CARD16 */ + +#include "xsettings-manager.h" + +#define XSETTINGS_VARIANT_TYPE_COLOR (G_VARIANT_TYPE ("(qqqq)")) + +struct _XSettingsManager +{ + Display *display; + int screen; + + Window window; + Atom manager_atom; + Atom selection_atom; + Atom xsettings_atom; + + XSettingsTerminateFunc terminate; + void *cb_data; + + GHashTable *settings; + unsigned long serial; + + GVariant *overrides; +}; + +typedef struct +{ + Window window; + Atom timestamp_prop_atom; +} TimeStampInfo; + +static Bool +timestamp_predicate (Display *display, + XEvent *xevent, + XPointer arg) +{ + TimeStampInfo *info = (TimeStampInfo *)arg; + + if (xevent->type == PropertyNotify && + xevent->xproperty.window == info->window && + xevent->xproperty.atom == info->timestamp_prop_atom) + return True; + + return False; +} + +/** + * get_server_time: + * @display: display from which to get the time + * @window: a #Window, used for communication with the server. + * The window must have PropertyChangeMask in its + * events mask or a hang will result. + * + * Routine to get the current X server time stamp. + * + * Return value: the time stamp. + **/ +static Time +get_server_time (Display *display, + Window window) +{ + unsigned char c = 'a'; + XEvent xevent; + TimeStampInfo info; + + info.timestamp_prop_atom = XInternAtom (display, "_TIMESTAMP_PROP", False); + info.window = window; + + XChangeProperty (display, window, + info.timestamp_prop_atom, info.timestamp_prop_atom, + 8, PropModeReplace, &c, 1); + + XIfEvent (display, &xevent, + timestamp_predicate, (XPointer)&info); + + return xevent.xproperty.time; +} + +Bool +xsettings_manager_check_running (Display *display, + int screen) +{ + char buffer[256]; + Atom selection_atom; + + sprintf(buffer, "_XSETTINGS_S%d", screen); + selection_atom = XInternAtom (display, buffer, False); + + if (XGetSelectionOwner (display, selection_atom)) + return True; + else + return False; +} + +XSettingsManager * +xsettings_manager_new (Display *display, + int screen, + XSettingsTerminateFunc terminate, + void *cb_data) +{ + XSettingsManager *manager; + Time timestamp; + XClientMessageEvent xev; + + char buffer[256]; + + manager = g_slice_new (XSettingsManager); + + manager->display = display; + manager->screen = screen; + + sprintf(buffer, "_XSETTINGS_S%d", screen); + manager->selection_atom = XInternAtom (display, buffer, False); + manager->xsettings_atom = XInternAtom (display, "_XSETTINGS_SETTINGS", False); + manager->manager_atom = XInternAtom (display, "MANAGER", False); + + manager->terminate = terminate; + manager->cb_data = cb_data; + + manager->settings = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify) xsettings_setting_free); + manager->serial = 0; + manager->overrides = NULL; + + manager->window = XCreateSimpleWindow (display, + RootWindow (display, screen), + 0, 0, 10, 10, 0, + WhitePixel (display, screen), + WhitePixel (display, screen)); + + XSelectInput (display, manager->window, PropertyChangeMask); + timestamp = get_server_time (display, manager->window); + + XSetSelectionOwner (display, manager->selection_atom, + manager->window, timestamp); + + /* Check to see if we managed to claim the selection. If not, + * we treat it as if we got it then immediately lost it + */ + + if (XGetSelectionOwner (display, manager->selection_atom) == + manager->window) + { + xev.type = ClientMessage; + xev.window = RootWindow (display, screen); + xev.message_type = manager->manager_atom; + xev.format = 32; + xev.data.l[0] = timestamp; + xev.data.l[1] = manager->selection_atom; + xev.data.l[2] = manager->window; + xev.data.l[3] = 0; /* manager specific data */ + xev.data.l[4] = 0; /* manager specific data */ + + XSendEvent (display, RootWindow (display, screen), + False, StructureNotifyMask, (XEvent *)&xev); + } + else + { + manager->terminate (manager->cb_data); + } + + return manager; +} + +void +xsettings_manager_destroy (XSettingsManager *manager) +{ + XDestroyWindow (manager->display, manager->window); + + g_hash_table_unref (manager->settings); + + g_slice_free (XSettingsManager, manager); +} + +static void +xsettings_manager_set_setting (XSettingsManager *manager, + const gchar *name, + gint tier, + GVariant *value) +{ + XSettingsSetting *setting; + + setting = g_hash_table_lookup (manager->settings, name); + + if (setting == NULL) + { + setting = xsettings_setting_new (name); + setting->last_change_serial = manager->serial; + g_hash_table_insert (manager->settings, setting->name, setting); + } + + xsettings_setting_set (setting, tier, value, manager->serial); + + if (xsettings_setting_get (setting) == NULL) + g_hash_table_remove (manager->settings, name); +} + +void +xsettings_manager_set_int (XSettingsManager *manager, + const char *name, + int value) +{ + xsettings_manager_set_setting (manager, name, 0, g_variant_new_int32 (value)); +} + +void +xsettings_manager_set_string (XSettingsManager *manager, + const char *name, + const char *value) +{ + xsettings_manager_set_setting (manager, name, 0, g_variant_new_string (value)); +} + +void +xsettings_manager_set_color (XSettingsManager *manager, + const char *name, + XSettingsColor *value) +{ + GVariant *tmp; + + tmp = g_variant_new ("(qqqq)", value->red, value->green, value->blue, value->alpha); + g_assert (g_variant_is_of_type (tmp, XSETTINGS_VARIANT_TYPE_COLOR)); /* paranoia... */ + xsettings_manager_set_setting (manager, name, 0, tmp); +} + +void +xsettings_manager_delete_setting (XSettingsManager *manager, + const char *name) +{ + xsettings_manager_set_setting (manager, name, 0, NULL); +} + +static gchar +xsettings_get_typecode (GVariant *value) +{ + switch (g_variant_classify (value)) + { + case G_VARIANT_CLASS_INT32: + return XSETTINGS_TYPE_INT; + case G_VARIANT_CLASS_STRING: + return XSETTINGS_TYPE_STRING; + case G_VARIANT_CLASS_TUPLE: + return XSETTINGS_TYPE_COLOR; + default: + g_assert_not_reached (); + } +} + +static void +align_string (GString *string, + gint alignment) +{ + /* Adds nul-bytes to the string until its length is an even multiple + * of the specified alignment requirement. + */ + while ((string->len % alignment) != 0) + g_string_append_c (string, '\0'); +} + +static void +setting_store (XSettingsSetting *setting, + GString *buffer) +{ + XSettingsType type; + GVariant *value; + guint16 len16; + + value = xsettings_setting_get (setting); + + type = xsettings_get_typecode (value); + + g_string_append_c (buffer, type); + g_string_append_c (buffer, 0); + + len16 = strlen (setting->name); + g_string_append_len (buffer, (gchar *) &len16, 2); + g_string_append (buffer, setting->name); + align_string (buffer, 4); + + g_string_append_len (buffer, (gchar *) &setting->last_change_serial, 4); + + if (type == XSETTINGS_TYPE_STRING) + { + const gchar *string; + gsize stringlen; + guint32 len32; + + string = g_variant_get_string (value, &stringlen); + len32 = stringlen; + g_string_append_len (buffer, (gchar *) &len32, 4); + g_string_append (buffer, string); + align_string (buffer, 4); + } + else + /* GVariant format is the same as XSETTINGS format for the non-string types */ + g_string_append_len (buffer, g_variant_get_data (value), g_variant_get_size (value)); +} + +void +xsettings_manager_notify (XSettingsManager *manager) +{ + GString *buffer; + GHashTableIter iter; + int n_settings; + gpointer value; + + n_settings = g_hash_table_size (manager->settings); + + buffer = g_string_new (NULL); + g_string_append_c (buffer, xsettings_byte_order ()); + g_string_append_c (buffer, '\0'); + g_string_append_c (buffer, '\0'); + g_string_append_c (buffer, '\0'); + + g_string_append_len (buffer, (gchar *) &manager->serial, 4); + g_string_append_len (buffer, (gchar *) &n_settings, 4); + + g_hash_table_iter_init (&iter, manager->settings); + while (g_hash_table_iter_next (&iter, NULL, &value)) + setting_store (value, buffer); + + XChangeProperty (manager->display, manager->window, + manager->xsettings_atom, manager->xsettings_atom, + 8, PropModeReplace, (guchar *) buffer->str, buffer->len); + + g_string_free (buffer, TRUE); + manager->serial++; +} + +void +xsettings_manager_set_overrides (XSettingsManager *manager, + GVariant *overrides) +{ + GVariantIter iter; + const gchar *key; + GVariant *value; + + g_return_if_fail (overrides != NULL && g_variant_is_of_type (overrides, G_VARIANT_TYPE_VARDICT)); + + if (manager->overrides) + { + /* unset the existing overrides */ + + g_variant_iter_init (&iter, manager->overrides); + while (g_variant_iter_next (&iter, "{&sv}", &key, NULL)) + /* only unset it at this point if it's not in the new list */ + if (!g_variant_lookup (overrides, key, "*", NULL)) + xsettings_manager_set_setting (manager, key, 1, NULL); + g_variant_unref (manager->overrides); + } + + /* save this so we can do the unsets next time */ + manager->overrides = g_variant_ref_sink (overrides); + + /* set the new values */ + g_variant_iter_init (&iter, overrides); + while (g_variant_iter_loop (&iter, "{&sv}", &key, &value)) + { + /* only accept recognised types... */ + if (!g_variant_is_of_type (value, G_VARIANT_TYPE_STRING) && + !g_variant_is_of_type (value, G_VARIANT_TYPE_INT32) && + !g_variant_is_of_type (value, XSETTINGS_VARIANT_TYPE_COLOR)) + continue; + + xsettings_manager_set_setting (manager, key, 1, value); + } +} diff --git a/plugins/xsettings/xsettings-manager.h b/plugins/xsettings/xsettings-manager.h new file mode 100644 index 0000000..a8cf151 --- /dev/null +++ b/plugins/xsettings/xsettings-manager.h @@ -0,0 +1,58 @@ +/* + * Copyright © 2001 Red Hat, Inc. + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation, and that the name of Red Hat not be used in advertising or + * publicity pertaining to distribution of the software without specific, + * written prior permission. Red Hat makes no representations about the + * suitability of this software for any purpose. It is provided "as is" + * without express or implied warranty. + * + * RED HAT DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL RED HAT + * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Author: Owen Taylor, Red Hat, Inc. + */ +#ifndef XSETTINGS_MANAGER_H +#define XSETTINGS_MANAGER_H + +#include <X11/Xlib.h> +#include "xsettings-common.h" + +typedef struct _XSettingsManager XSettingsManager; + +typedef void (*XSettingsTerminateFunc) (void *cb_data); + +Bool xsettings_manager_check_running (Display *display, + int screen); + +XSettingsManager *xsettings_manager_new (Display *display, + int screen, + XSettingsTerminateFunc terminate, + void *cb_data); + +void xsettings_manager_destroy (XSettingsManager *manager); + +void xsettings_manager_delete_setting (XSettingsManager *manager, + const char *name); +void xsettings_manager_set_int (XSettingsManager *manager, + const char *name, + int value); +void xsettings_manager_set_string (XSettingsManager *manager, + const char *name, + const char *value); +void xsettings_manager_set_color (XSettingsManager *manager, + const char *name, + XSettingsColor *value); +void xsettings_manager_notify (XSettingsManager *manager); +void xsettings_manager_set_overrides (XSettingsManager *manager, + GVariant *overrides); + +#endif /* XSETTINGS_MANAGER_H */ |