From b0e30ceba2288eab10c6ff7be0ac0cb05a9ed0b7 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:51:51 +0200 Subject: Adding upstream version 43.0. Signed-off-by: Daniel Baumann --- plugins/media-keys/gsd-media-keys-manager.c | 3553 +++++++++++++++++++++++++++ 1 file changed, 3553 insertions(+) create mode 100644 plugins/media-keys/gsd-media-keys-manager.c (limited to 'plugins/media-keys/gsd-media-keys-manager.c') 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..15e96e0 --- /dev/null +++ b/plugins/media-keys/gsd-media-keys-manager.c @@ -0,0 +1,3553 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2001-2003 Bastien Nocera + * Copyright (C) 2006-2007 William Jon McCann + * + * 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 . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include + +#if HAVE_GUDEV +#include +#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-input-helper.h" +#include "gsd-enums.h" +#include "gsd-shell-helper.h" + +#include +#include +#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 */ + +#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 REWIND_USEC (-10 * G_USEC_PER_SEC) +#define FASTFORWARD_USEC (45 * G_USEC_PER_SEC) + +#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; + + /* Rotation */ + guint iio_sensor_watch_id; + gboolean has_accel; + GDBusProxy *iio_sensor_proxy; + + /* RFKill stuff */ + guint rfkill_watch_id; + guint64 rfkill_last_time; + GDBusProxy *rfkill_proxy; + GCancellable *rfkill_cancellable; + + /* systemd stuff */ + GDBusProxy *logind_proxy; + gint inhibit_keys_fd; + gint inhibit_suspend_fd; + gboolean inhibit_suspend_taken; + + GDBusConnection *connection; + GCancellable *bus_cancellable; + + guint start_idle_id; + + /* Multimedia keys */ + 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; + + /* 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, "[OUT] Speaker") != 0 && + g_strcmp0 (port->port, "[OUT] Handset") != 0 && + 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 gboolean +do_multimedia_player_action (GsdMediaKeysManager *manager, + const char *key) +{ + GsdMediaKeysManagerPrivate *priv = GSD_MEDIA_KEYS_MANAGER_GET_PRIVATE (manager); + + g_return_val_if_fail (key != NULL, FALSE); + + g_debug ("Media key '%s' pressed", key); + + if (mpris_controller_get_has_active_player (priv->mpris_controller)) { + if (g_str_equal (key, "Rewind")) { + if (mpris_controller_seek (priv->mpris_controller, REWIND_USEC)) + return TRUE; + } else if (g_str_equal (key, "FastForward")) { + if (mpris_controller_seek (priv->mpris_controller, FASTFORWARD_USEC)) + return TRUE; + } else if (g_str_equal (key, "Repeat")) { + if (mpris_controller_toggle (priv->mpris_controller, "LoopStatus")) + return TRUE; + } else if (g_str_equal (key, "Shuffle")) { + if (mpris_controller_toggle (priv->mpris_controller, "Shuffle")) + return TRUE; + } else if (mpris_controller_key (priv->mpris_controller, key)) { + return TRUE; + } + } + + /* Popup a dialog with an (/) icon */ + show_osd (manager, "action-unavailable-symbolic", NULL, -1, NULL); + return TRUE; +} + +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; + default: + g_warn_if_reached (); + G_GNUC_FALLTHROUGH; + 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; + guint64 current_time; + RfkillData *data; + + has_mode = bluetooth ? "BluetoothHasAirplaneMode" : "HasAirplaneMode"; + hw_mode = bluetooth ? "BluetoothHardwareAirplaneMode" : "HardwareAirplaneMode"; + mode = bluetooth ? "BluetoothAirplaneMode" : "AirplaneMode"; + + if (priv->rfkill_proxy == NULL) + return; + + /* Some hardwares can generate multiple rfkill events from different + * drivers, on a single hotkey press. Only process the first event and + * debounce the others */ + current_time = g_get_monotonic_time (); + if (current_time - priv->rfkill_last_time < G_USEC_PER_SEC) + return; + + priv->rfkill_last_time = current_time; + + 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 +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 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, "org.gnome.Settings.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_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); + + 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->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); + + 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 }, + { "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->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->iio_sensor_proxy); + g_clear_pointer (&priv->chassis_type, g_free); + 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->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); + 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_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->bus_cancellable = g_cancellable_new (); + 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); +} -- cgit v1.2.3