diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 11 | ||||
-rw-r--r-- | src/accounts-service-access.vala | 406 | ||||
-rw-r--r-- | src/accounts-service-user.vala | 458 | ||||
-rw-r--r-- | src/freedesktop-interfaces.vala | 18 | ||||
-rw-r--r-- | src/greeter-broadcast.vala | 8 | ||||
-rw-r--r-- | src/media-player-list-greeter.vala | 203 | ||||
-rw-r--r-- | src/media-player-list-mpris.vala | 220 | ||||
-rw-r--r-- | src/media-player-mpris.vala | 592 | ||||
-rw-r--r-- | src/media-player-user.vala | 563 | ||||
-rw-r--r-- | src/mpris2-interfaces.vala | 23 | ||||
-rw-r--r-- | src/sound-menu.vala | 4 | ||||
-rw-r--r-- | src/volume-control-pulse.vala | 1677 | ||||
-rw-r--r-- | src/volume-warning.vala | 410 |
13 files changed, 2321 insertions, 2272 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3630753..60eb961 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,7 +29,7 @@ if(EXISTS "/usr/share/accountsservice/interfaces/com.ubuntu.touch.AccountsServic set (VALA_DEFINE_ACCTSERVICE_SYSTEMSOUND_SETTINGS "--define=HAS_UT_ACCTSERVICE_SYSTEMSOUND_SETTINGS") else() set (HAVE_UT_ACCTSERVICE_SYSTEMSOUND_SETTINGS OFF) -endif() +endif() if(EXISTS "/usr/share/accountsservice/interfaces/com.ubuntu.AccountsService.Sound.xml") set (HAVE_UT_ACCTSERVICE_SOUND_SETTINGS ON) @@ -63,7 +63,6 @@ vala_init(ayatana-indicator-sound-service ${VALA_PKG_URLDISPATCHER} OPTIONS --ccode - --thread --target-glib=${GLIB_2_0_REQUIRED_VERSION} --vapidir=${CMAKE_SOURCE_DIR}/vapi/ --vapidir=. @@ -247,15 +246,15 @@ vala_add(ayatana-indicator-sound-service ) if(${HAVE_UT_ACCTSERVICE_PRIVACY_SETTINGS}) - vala_add(ayatana-indicator-sound-service + vala_add(ayatana-indicator-sound-service accounts-service-system-sound-settings.vala - ) + ) endif() if(${HAVE_UT_ACCTSERVICE_SYSTEMSOUND_SETTINGS}) - vala_add(ayatana-indicator-sound-service + vala_add(ayatana-indicator-sound-service accounts-service-privacy-settings.vala - ) + ) endif() vala_add(ayatana-indicator-sound-service diff --git a/src/accounts-service-access.vala b/src/accounts-service-access.vala index 57c625e..fc634ab 100644 --- a/src/accounts-service-access.vala +++ b/src/accounts-service-access.vala @@ -1,6 +1,7 @@ /* * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*- * Copyright 2016 Canonical Ltd. + * Copyright 2021 Robert Tari * * 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 @@ -16,6 +17,7 @@ * * Authors: * Xavi Garcia <xavi.garcia.mena@canonical.com> + * Robert Tari <robert@tari.in> */ using PulseAudio; @@ -25,213 +27,213 @@ using Gee; [DBus (name="org.ayatana.Greeter.List")] interface GreeterListInterfaceAccess : Object { - public abstract async string get_active_entry () throws IOError; - public signal void entry_selected (string entry_name); + public abstract async string get_active_entry () throws GLib.DBusError, GLib.IOError; + public signal void entry_selected (string entry_name); } public class AccountsServiceAccess : Object { - private DBusProxy _user_proxy; - private GreeterListInterfaceAccess _greeter_proxy; - private double _volume = 0.0; - private string _last_running_player = ""; - private bool _mute = false; - private Cancellable _dbus_call_cancellable; - - public AccountsServiceAccess () - { - _dbus_call_cancellable = new Cancellable (); - setup_accountsservice.begin (); - } - - ~AccountsServiceAccess () - { - _dbus_call_cancellable.cancel (); - } - - public string last_running_player - { - get - { - return _last_running_player; - } - set - { - sync_last_running_player_to_accountsservice.begin (value); - } - } - - public bool mute - { - get - { - return _mute; - } - set - { - sync_mute_to_accountsservice.begin (value); - } - } - - public double volume - { - get - { - return _volume; - } - set - { - sync_volume_to_accountsservice.begin (value); - } - } - - /* AccountsService operations */ - private void accountsservice_props_changed_cb (DBusProxy proxy, Variant changed_properties, string[]? invalidated_properties) - { - Variant volume_variant = changed_properties.lookup_value ("Volume", VariantType.DOUBLE); - if (volume_variant != null) { - var volume = volume_variant.get_double (); - if (volume >= 0 && _volume != volume) { - _volume = volume; - this.notify_property("volume"); - } - } - - Variant mute_variant = changed_properties.lookup_value ("Muted", VariantType.BOOLEAN); - if (mute_variant != null) { - _mute = mute_variant.get_boolean (); - this.notify_property("mute"); - } - - Variant last_running_player_variant = changed_properties.lookup_value ("LastRunningPlayer", VariantType.STRING); - if (last_running_player_variant != null) { - _last_running_player = last_running_player_variant.get_string (); - this.notify_property("last-running-player"); - } - } - - private async void setup_user_proxy (string? username_in = null) - { - var username = username_in; - _user_proxy = null; - - // Look up currently selected greeter user, if asked - if (username == null) { - try { - username = yield _greeter_proxy.get_active_entry (); - if (username == "" || username == null) - return; - } catch (GLib.Error e) { - warning ("unable to find Accounts path for user %s: %s", username == null ? "null" : username, e.message); - return; - } - } - - // Get master AccountsService object - DBusProxy accounts_proxy; - try { - accounts_proxy = yield DBusProxy.create_for_bus (BusType.SYSTEM, DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, null, "org.freedesktop.Accounts", "/org/freedesktop/Accounts", "org.freedesktop.Accounts"); - } catch (GLib.Error e) { - warning ("unable to get greeter proxy: %s", e.message); - return; - } - - // Find user's AccountsService object - try { - var user_path_variant = yield accounts_proxy.call ("FindUserByName", new Variant ("(s)", username), DBusCallFlags.NONE, -1); - string user_path; - if (user_path_variant.check_format_string ("(o)", true)) { - user_path_variant.get ("(o)", out user_path); + private DBusProxy _user_proxy; + private GreeterListInterfaceAccess _greeter_proxy; + private double _volume = 0.0; + private string _last_running_player = ""; + private bool _mute = false; + private Cancellable _dbus_call_cancellable; + + public AccountsServiceAccess () + { + _dbus_call_cancellable = new Cancellable (); + setup_accountsservice.begin (); + } + + ~AccountsServiceAccess () + { + _dbus_call_cancellable.cancel (); + } + + public string last_running_player + { + get + { + return _last_running_player; + } + set + { + sync_last_running_player_to_accountsservice.begin (value); + } + } + + public bool mute + { + get + { + return _mute; + } + set + { + sync_mute_to_accountsservice.begin (value); + } + } + + public double volume + { + get + { + return _volume; + } + set + { + sync_volume_to_accountsservice.begin (value); + } + } + + /* AccountsService operations */ + private void accountsservice_props_changed_cb (DBusProxy proxy, Variant changed_properties, string[]? invalidated_properties) + { + Variant volume_variant = changed_properties.lookup_value ("Volume", VariantType.DOUBLE); + if (volume_variant != null) { + var volume = volume_variant.get_double (); + if (volume >= 0 && _volume != volume) { + _volume = volume; + this.notify_property("volume"); + } + } + + Variant mute_variant = changed_properties.lookup_value ("Muted", VariantType.BOOLEAN); + if (mute_variant != null) { + _mute = mute_variant.get_boolean (); + this.notify_property("mute"); + } + + Variant last_running_player_variant = changed_properties.lookup_value ("LastRunningPlayer", VariantType.STRING); + if (last_running_player_variant != null) { + _last_running_player = last_running_player_variant.get_string (); + this.notify_property("last-running-player"); + } + } + + private async void setup_user_proxy (string? username_in = null) + { + var username = username_in; + _user_proxy = null; + + // Look up currently selected greeter user, if asked + if (username == null) { + try { + username = yield _greeter_proxy.get_active_entry (); + if (username == "" || username == null) + return; + } catch (GLib.Error e) { + warning ("unable to find Accounts path for user %s: %s", username == null ? "null" : username, e.message); + return; + } + } + + // Get master AccountsService object + DBusProxy accounts_proxy; + try { + accounts_proxy = yield new DBusProxy.for_bus (BusType.SYSTEM, DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, null, "org.freedesktop.Accounts", "/org/freedesktop/Accounts", "org.freedesktop.Accounts"); + } catch (GLib.Error e) { + warning ("unable to get greeter proxy: %s", e.message); + return; + } + + // Find user's AccountsService object + try { + var user_path_variant = yield accounts_proxy.call ("FindUserByName", new Variant ("(s)", username), DBusCallFlags.NONE, -1); + string user_path; + if (user_path_variant.check_format_string ("(o)", true)) { + user_path_variant.get ("(o)", out user_path); #if HAS_UT_ACCTSERVICE_SOUND_SETTINGS - _user_proxy = yield DBusProxy.create_for_bus (BusType.SYSTEM, DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, "org.freedesktop.Accounts", user_path, "com.ubuntu.AccountsService.Sound"); + _user_proxy = yield new DBusProxy.for_bus (BusType.SYSTEM, DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, "org.freedesktop.Accounts", user_path, "com.ubuntu.AccountsService.Sound"); #else - _user_proxy = yield DBusProxy.create_for_bus (BusType.SYSTEM, DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, "org.freedesktop.Accounts", user_path, "org.ayatana.AccountsService.Sound"); + _user_proxy = yield new DBusProxy.for_bus (BusType.SYSTEM, DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, "org.freedesktop.Accounts", user_path, "org.ayatana.AccountsService.Sound"); #endif - } else { - warning ("Unable to find user name after calling FindUserByName. Expected type: %s and obtained %s", "(o)", user_path_variant.get_type_string () ); - return; - } - } catch (GLib.Error e) { - warning ("unable to find Accounts path for user %s: %s", username, e.message); - return; - } - - // Get current values and listen for changes - _user_proxy.g_properties_changed.connect (accountsservice_props_changed_cb); - try { - var props_variant = yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "GetAll", new Variant ("(s)", _user_proxy.get_interface_name ()), null, DBusCallFlags.NONE, -1); - if (props_variant.check_format_string ("(@a{sv})", true)) { - Variant props; - props_variant.get ("(@a{sv})", out props); - accountsservice_props_changed_cb(_user_proxy, props, null); - } else { - warning ("Unable to get accounts service properties after calling GetAll. Expected type: %s and obtained %s", "(@a{sv})", props_variant.get_type_string () ); - return; - } - } catch (GLib.Error e) { - debug("Unable to get properties for user %s at first try: %s", username, e.message); - } - } - - private void greeter_user_changed (string username) - { - setup_user_proxy.begin (username); - } - - private async void setup_accountsservice () - { - if (Environment.get_variable ("XDG_SESSION_CLASS") == "greeter") { - try { - _greeter_proxy = yield Bus.get_proxy (BusType.SESSION, "org.ayatana.Greeter", "/list"); - } catch (GLib.Error e) { - warning ("unable to get greeter proxy: %s", e.message); - return; - } - _greeter_proxy.entry_selected.connect (greeter_user_changed); - yield setup_user_proxy (); - } else { - // We are in a user session. We just need our own proxy - unowned string username = Environment.get_variable ("USER"); - if (username != null && username != "") { - yield setup_user_proxy (username); - } - } - } - - private async void sync_last_running_player_to_accountsservice (string last_running_player) - { - if (_user_proxy == null) - return; - - try { - yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "Set", new Variant ("(ssv)", _user_proxy.get_interface_name (), "LastRunningPlayer", new Variant ("s", last_running_player)), null, DBusCallFlags.NONE, -1, _dbus_call_cancellable); - } catch (GLib.Error e) { - warning ("unable to sync last running player %s to AccountsService: %s",last_running_player, e.message); - } - _last_running_player = last_running_player; - } - - private async void sync_volume_to_accountsservice (double volume) - { - if (_user_proxy == null) - return; - - try { - yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "Set", new Variant ("(ssv)", _user_proxy.get_interface_name (), "Volume", new Variant ("d", volume)), null, DBusCallFlags.NONE, -1, _dbus_call_cancellable); - } catch (GLib.Error e) { - warning ("unable to sync volume %f to AccountsService: %s", volume, e.message); - } - } - - private async void sync_mute_to_accountsservice (bool mute) - { - if (_user_proxy == null) - return; - - try { - yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "Set", new Variant ("(ssv)", _user_proxy.get_interface_name (), "Muted", new Variant ("b", mute)), null, DBusCallFlags.NONE, -1, _dbus_call_cancellable); - } catch (GLib.Error e) { - warning ("unable to sync mute %s to AccountsService: %s", mute ? "true" : "false", e.message); - } - } + } else { + warning ("Unable to find user name after calling FindUserByName. Expected type: %s and obtained %s", "(o)", user_path_variant.get_type_string () ); + return; + } + } catch (GLib.Error e) { + warning ("unable to find Accounts path for user %s: %s", username, e.message); + return; + } + + // Get current values and listen for changes + _user_proxy.g_properties_changed.connect (accountsservice_props_changed_cb); + try { + var props_variant = yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "GetAll", new Variant ("(s)", _user_proxy.get_interface_name ()), null, DBusCallFlags.NONE, -1); + if (props_variant.check_format_string ("(@a{sv})", true)) { + Variant props; + props_variant.get ("(@a{sv})", out props); + accountsservice_props_changed_cb(_user_proxy, props, null); + } else { + warning ("Unable to get accounts service properties after calling GetAll. Expected type: %s and obtained %s", "(@a{sv})", props_variant.get_type_string () ); + return; + } + } catch (GLib.Error e) { + debug("Unable to get properties for user %s at first try: %s", username, e.message); + } + } + + private void greeter_user_changed (string username) + { + setup_user_proxy.begin (username); + } + + private async void setup_accountsservice () + { + if (Environment.get_variable ("XDG_SESSION_CLASS") == "greeter") { + try { + _greeter_proxy = yield Bus.get_proxy (BusType.SESSION, "org.ayatana.Greeter", "/list"); + } catch (GLib.Error e) { + warning ("unable to get greeter proxy: %s", e.message); + return; + } + _greeter_proxy.entry_selected.connect (greeter_user_changed); + yield setup_user_proxy (); + } else { + // We are in a user session. We just need our own proxy + unowned string username = Environment.get_variable ("USER"); + if (username != null && username != "") { + yield setup_user_proxy (username); + } + } + } + + private async void sync_last_running_player_to_accountsservice (string last_running_player) + { + if (_user_proxy == null) + return; + + try { + yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "Set", new Variant ("(ssv)", _user_proxy.get_interface_name (), "LastRunningPlayer", new Variant ("s", last_running_player)), null, DBusCallFlags.NONE, -1, _dbus_call_cancellable); + } catch (GLib.Error e) { + warning ("unable to sync last running player %s to AccountsService: %s",last_running_player, e.message); + } + _last_running_player = last_running_player; + } + + private async void sync_volume_to_accountsservice (double volume) + { + if (_user_proxy == null) + return; + + try { + yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "Set", new Variant ("(ssv)", _user_proxy.get_interface_name (), "Volume", new Variant ("d", volume)), null, DBusCallFlags.NONE, -1, _dbus_call_cancellable); + } catch (GLib.Error e) { + warning ("unable to sync volume %f to AccountsService: %s", volume, e.message); + } + } + + private async void sync_mute_to_accountsservice (bool mute) + { + if (_user_proxy == null) + return; + + try { + yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "Set", new Variant ("(ssv)", _user_proxy.get_interface_name (), "Muted", new Variant ("b", mute)), null, DBusCallFlags.NONE, -1, _dbus_call_cancellable); + } catch (GLib.Error e) { + warning ("unable to sync mute %s to AccountsService: %s", mute ? "true" : "false", e.message); + } + } } diff --git a/src/accounts-service-user.vala b/src/accounts-service-user.vala index 25b19ee..fee5fe9 100644 --- a/src/accounts-service-user.vala +++ b/src/accounts-service-user.vala @@ -18,251 +18,251 @@ */ public class AccountsServiceUser : Object { - Act.UserManager accounts_manager = Act.UserManager.get_default(); - Act.User? user = null; - AccountsServiceSoundSettings? proxy = null; + Act.UserManager accounts_manager = Act.UserManager.get_default(); + Act.User? user = null; + AccountsServiceSoundSettings? proxy = null; #if HAS_UT_ACCTSERVICE_PRIVACY_SETTINGS - AccountsServicePrivacySettings? privacyproxy = null; + AccountsServicePrivacySettings? privacyproxy = null; #endif #if HAS_UT_ACCTSERVICE_SYSTEMSOUND_SETTINGS - AccountsServiceSystemSoundSettings? syssoundproxy = null; + AccountsServiceSystemSoundSettings? syssoundproxy = null; #endif - uint timer = 0; - MediaPlayer? _player = null; - GreeterBroadcast? greeter = null; - - public bool showDataOnGreeter { get; set; } - - bool _silentMode = false; - public bool silentMode { - get { - return _silentMode; - } - set { - _silentMode = value; + uint timer = 0; + MediaPlayer? _player = null; + GreeterBroadcast? greeter = null; + + public bool showDataOnGreeter { get; set; } + + bool _silentMode = false; + public bool silentMode { + get { + return _silentMode; + } + set { + _silentMode = value; #if HAS_UT_ACCTSERVICE_SYSTEMSOUND_SETTINGS - if (syssoundproxy != null) - syssoundproxy.silent_mode = value; + if (syssoundproxy != null) + syssoundproxy.silent_mode = value; #endif - } - } - - public MediaPlayer? player { - set { - this._player = value; - debug("New player: %s", this._player != null ? this._player.name : "Cleared"); - - /* No proxy, no settings to set */ - if (this.proxy == null) { - debug("Nothing written to Accounts Service, waiting on proxy"); - return; - } - - /* Always reset the timer */ - if (this.timer != 0) { - GLib.Source.remove(this.timer); - this.timer = 0; - } - - if (this._player == null) { - debug("Clearing player data in accounts service"); - - /* Clear it */ - this.proxy.player_name = ""; - this.proxy.timestamp = 0; - this.proxy.title = ""; - this.proxy.artist = ""; - this.proxy.album = ""; - this.proxy.art_url = ""; - - var icon = new ThemedIcon.with_default_fallbacks ("application-default-icon"); - this.proxy.player_icon = icon.serialize(); - } else { - this.proxy.timestamp = GLib.get_monotonic_time(); - this.proxy.player_name = this._player.name; - - /* Serialize the icon if it exits, if it doesn't or errors then - we need to use the application default icon */ - GLib.Variant? icon_serialization = null; - if (this._player.icon != null) - icon_serialization = this._player.icon.serialize(); - if (icon_serialization == null) { - var icon = new ThemedIcon.with_default_fallbacks ("application-default-icon"); - icon_serialization = icon.serialize(); - } - this.proxy.player_icon = icon_serialization; - - /* Set state of the player */ - this.proxy.running = this._player.is_running; - this.proxy.state = this._player.state; - - if (this._player.current_track != null) { - this.proxy.title = this._player.current_track.title; - this.proxy.artist = this._player.current_track.artist; - this.proxy.album = this._player.current_track.album; - this.proxy.art_url = this._player.current_track.art_url; - } else { - this.proxy.title = ""; - this.proxy.artist = ""; - this.proxy.album = ""; - this.proxy.art_url = ""; - } - - this.timer = GLib.Timeout.add_seconds(5 * 60, () => { - debug("Writing timestamp"); - this.proxy.timestamp = GLib.get_monotonic_time(); - return true; - }); - } - } - get { - return this._player; - } - } - - public AccountsServiceUser () { - user = accounts_manager.get_user(GLib.Environment.get_user_name()); - user.notify["is-loaded"].connect(() => user_loaded_changed()); - user_loaded_changed(); - - Bus.get_proxy.begin<GreeterBroadcast> ( - BusType.SYSTEM, - "org.ayatana.Desktop.Greeter.Broadcast", - "/org/ayatana/Desktop/Greeter/Broadcast", - DBusProxyFlags.NONE, - null, - greeter_proxy_new); - } - - void user_loaded_changed () { - debug("User loaded changed"); - - this.proxy = null; - - if (this.user.is_loaded) { - Bus.get_proxy.begin<AccountsServiceSoundSettings> ( - BusType.SYSTEM, - "org.freedesktop.Accounts", - user.get_object_path(), - DBusProxyFlags.GET_INVALIDATED_PROPERTIES, - null, - new_sound_proxy); + } + } + + public MediaPlayer? player { + set { + this._player = value; + debug("New player: %s", this._player != null ? this._player.name : "Cleared"); + + /* No proxy, no settings to set */ + if (this.proxy == null) { + debug("Nothing written to Accounts Service, waiting on proxy"); + return; + } + + /* Always reset the timer */ + if (this.timer != 0) { + GLib.Source.remove(this.timer); + this.timer = 0; + } + + if (this._player == null) { + debug("Clearing player data in accounts service"); + + /* Clear it */ + this.proxy.player_name = ""; + this.proxy.timestamp = 0; + this.proxy.title = ""; + this.proxy.artist = ""; + this.proxy.album = ""; + this.proxy.art_url = ""; + + var icon = new ThemedIcon.with_default_fallbacks ("application-default-icon"); + this.proxy.player_icon = icon.serialize(); + } else { + this.proxy.timestamp = GLib.get_monotonic_time(); + this.proxy.player_name = this._player.name; + + /* Serialize the icon if it exits, if it doesn't or errors then + we need to use the application default icon */ + GLib.Variant? icon_serialization = null; + if (this._player.icon != null) + icon_serialization = this._player.icon.serialize(); + if (icon_serialization == null) { + var icon = new ThemedIcon.with_default_fallbacks ("application-default-icon"); + icon_serialization = icon.serialize(); + } + this.proxy.player_icon = icon_serialization; + + /* Set state of the player */ + this.proxy.running = this._player.is_running; + this.proxy.state = this._player.state; + + if (this._player.current_track != null) { + this.proxy.title = this._player.current_track.title; + this.proxy.artist = this._player.current_track.artist; + this.proxy.album = this._player.current_track.album; + this.proxy.art_url = this._player.current_track.art_url; + } else { + this.proxy.title = ""; + this.proxy.artist = ""; + this.proxy.album = ""; + this.proxy.art_url = ""; + } + + this.timer = GLib.Timeout.add_seconds(5 * 60, () => { + debug("Writing timestamp"); + this.proxy.timestamp = GLib.get_monotonic_time(); + return true; + }); + } + } + get { + return this._player; + } + } + + public AccountsServiceUser () { + user = accounts_manager.get_user(GLib.Environment.get_user_name()); + user.notify["is-loaded"].connect(() => user_loaded_changed()); + user_loaded_changed(); + + Bus.get_proxy.begin<GreeterBroadcast> ( + BusType.SYSTEM, + "org.ayatana.Desktop.Greeter.Broadcast", + "/org/ayatana/Desktop/Greeter/Broadcast", + DBusProxyFlags.NONE, + null, + greeter_proxy_new); + } + + void user_loaded_changed () { + debug("User loaded changed"); + + this.proxy = null; + + if (this.user.is_loaded) { + Bus.get_proxy.begin<AccountsServiceSoundSettings> ( + BusType.SYSTEM, + "org.freedesktop.Accounts", + user.get_object_path(), + DBusProxyFlags.GET_INVALIDATED_PROPERTIES, + null, + new_sound_proxy); #if HAS_UT_ACCTSERVICE_PRIVACY_SETTINGS - Bus.get_proxy.begin<AccountsServicePrivacySettings> ( - BusType.SYSTEM, - "org.freedesktop.Accounts", - user.get_object_path(), - DBusProxyFlags.GET_INVALIDATED_PROPERTIES, - null, - new_privacy_proxy); + Bus.get_proxy.begin<AccountsServicePrivacySettings> ( + BusType.SYSTEM, + "org.freedesktop.Accounts", + user.get_object_path(), + DBusProxyFlags.GET_INVALIDATED_PROPERTIES, + null, + new_privacy_proxy); #endif #if HAS_UT_ACCTSERVICE_SYSTEMSOUND_SETTINGS - Bus.get_proxy.begin<AccountsServiceSystemSoundSettings> ( - BusType.SYSTEM, - "org.freedesktop.Accounts", - user.get_object_path(), - DBusProxyFlags.GET_INVALIDATED_PROPERTIES, - null, - new_system_sound_proxy); + Bus.get_proxy.begin<AccountsServiceSystemSoundSettings> ( + BusType.SYSTEM, + "org.freedesktop.Accounts", + user.get_object_path(), + DBusProxyFlags.GET_INVALIDATED_PROPERTIES, + null, + new_system_sound_proxy); #endif - } - } - - ~AccountsServiceUser () { - debug("Account Service Object Finalizing"); - this.player = null; - - if (this.timer != 0) { - GLib.Source.remove(this.timer); - this.timer = 0; - } - } - - void new_sound_proxy (GLib.Object? obj, AsyncResult res) { - try { - this.proxy = Bus.get_proxy.end (res); - this.player = _player; - } catch (Error e) { - this.proxy = null; - warning("Unable to get proxy to user sound settings: %s", e.message); - } - } + } + } + + ~AccountsServiceUser () { + debug("Account Service Object Finalizing"); + this.player = null; + + if (this.timer != 0) { + GLib.Source.remove(this.timer); + this.timer = 0; + } + } + + void new_sound_proxy (GLib.Object? obj, AsyncResult res) { + try { + this.proxy = Bus.get_proxy.end (res); + this.player = _player; + } catch (Error e) { + this.proxy = null; + warning("Unable to get proxy to user sound settings: %s", e.message); + } + } #if HAS_UT_ACCTSERVICE_PRIVACY_SETTINGS - void new_privacy_proxy (GLib.Object? obj, AsyncResult res) { - try { - this.privacyproxy = Bus.get_proxy.end (res); - - (this.privacyproxy as DBusProxy).g_properties_changed.connect((proxy, changed, invalid) => { - var welcomeval = changed.lookup_value("MessagesWelcomeScreen", VariantType.BOOLEAN); - if (welcomeval != null) { - debug("Messages on welcome screen changed"); - this.showDataOnGreeter = welcomeval.get_boolean(); - } - }); - - this.showDataOnGreeter = this.privacyproxy.messages_welcome_screen; - } catch (Error e) { - this.privacyproxy = null; - warning("Unable to get proxy to user privacy settings: %s", e.message); - } - } + void new_privacy_proxy (GLib.Object? obj, AsyncResult res) { + try { + this.privacyproxy = Bus.get_proxy.end (res); + + (this.privacyproxy as DBusProxy).g_properties_changed.connect((proxy, changed, invalid) => { + var welcomeval = changed.lookup_value("MessagesWelcomeScreen", VariantType.BOOLEAN); + if (welcomeval != null) { + debug("Messages on welcome screen changed"); + this.showDataOnGreeter = welcomeval.get_boolean(); + } + }); + + this.showDataOnGreeter = this.privacyproxy.messages_welcome_screen; + } catch (Error e) { + this.privacyproxy = null; + warning("Unable to get proxy to user privacy settings: %s", e.message); + } + } #endif #if HAS_UT_ACCTSERVICE_SYSTEMSOUND_SETTINGS - void new_system_sound_proxy (GLib.Object? obj, AsyncResult res) { - try { - this.syssoundproxy = Bus.get_proxy.end (res); - - (this.syssoundproxy as DBusProxy).g_properties_changed.connect((proxy, changed, invalid) => { - var silentvar = changed.lookup_value("SilentMode", VariantType.BOOLEAN); - if (silentvar != null) { - debug("Silent Mode changed"); - this._silentMode = silentvar.get_boolean(); - this.notify_property("silentMode"); - } - }); - - this._silentMode = this.syssoundproxy.silent_mode; - this.notify_property("silentMode"); - } catch (Error e) { - this.syssoundproxy = null; - warning("Unable to get proxy to system sound settings: %s", e.message); - } - } + void new_system_sound_proxy (GLib.Object? obj, AsyncResult res) { + try { + this.syssoundproxy = Bus.get_proxy.end (res); + + (this.syssoundproxy as DBusProxy).g_properties_changed.connect((proxy, changed, invalid) => { + var silentvar = changed.lookup_value("SilentMode", VariantType.BOOLEAN); + if (silentvar != null) { + debug("Silent Mode changed"); + this._silentMode = silentvar.get_boolean(); + this.notify_property("silentMode"); + } + }); + + this._silentMode = this.syssoundproxy.silent_mode; + this.notify_property("silentMode"); + } catch (Error e) { + this.syssoundproxy = null; + warning("Unable to get proxy to system sound settings: %s", e.message); + } + } #endif - void greeter_proxy_new (GLib.Object? obj, AsyncResult res) { - try { - this.greeter = Bus.get_proxy.end (res); - - this.greeter.SoundPlayPause.connect((username) => { - if (username != GLib.Environment.get_user_name()) - return; - if (this._player == null) - return; - this._player.play_pause(); - }); - - this.greeter.SoundNext.connect((username) => { - if (username != GLib.Environment.get_user_name()) - return; - if (this._player == null) - return; - this._player.next(); - }); - - this.greeter.SoundPrev.connect((username) => { - if (username != GLib.Environment.get_user_name()) - return; - if (this._player == null) - return; - this._player.previous(); - }); - } catch (Error e) { - this.greeter = null; - warning("Unable to get greeter proxy: %s", e.message); - } - } + void greeter_proxy_new (GLib.Object? obj, AsyncResult res) { + try { + this.greeter = Bus.get_proxy.end (res); + + this.greeter.SoundPlayPause.connect((username) => { + if (username != GLib.Environment.get_user_name()) + return; + if (this._player == null) + return; + this._player.play_pause(); + }); + + this.greeter.SoundNext.connect((username) => { + if (username != GLib.Environment.get_user_name()) + return; + if (this._player == null) + return; + this._player.next(); + }); + + this.greeter.SoundPrev.connect((username) => { + if (username != GLib.Environment.get_user_name()) + return; + if (this._player == null) + return; + this._player.previous(); + }); + } catch (Error e) { + this.greeter = null; + warning("Unable to get greeter proxy: %s", e.message); + } + } } diff --git a/src/freedesktop-interfaces.vala b/src/freedesktop-interfaces.vala index b74f52b..b8fdfc7 100644 --- a/src/freedesktop-interfaces.vala +++ b/src/freedesktop-interfaces.vala @@ -1,25 +1,27 @@ /* Copyright 2010 Canonical Ltd. +Copyright 2021 Robert Tari Authors: Conor Curran <conor.curran@canonical.com> + Robert Tari <robert@tari.in> -This program is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License version 3, as published +This program is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. -This program is distributed in the hope that it will be useful, but -WITHOUT ANY WARRANTY; without even the implied warranties of -MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranties of +MERCHANTABILITY, SATISFACTORY QUALITY, 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 +You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ [DBus (name = "org.freedesktop.DBus")] public interface FreeDesktopObject: Object { - public abstract async string[] list_names() throws IOError; + public abstract async string[] list_names() throws GLib.DBusError, GLib.IOError; public abstract signal void name_owner_changed ( string name, string old_owner, string new_owner ); @@ -27,7 +29,7 @@ public interface FreeDesktopObject: Object { [DBus (name = "org.freedesktop.DBus.Introspectable")] public interface FreeDesktopIntrospectable: Object { - public abstract string Introspect() throws IOError; + public abstract string Introspect() throws GLib.DBusError, GLib.IOError; } [DBus (name = "org.freedesktop.DBus.Properties")] diff --git a/src/greeter-broadcast.vala b/src/greeter-broadcast.vala index 41caed8..b2f15d2 100644 --- a/src/greeter-broadcast.vala +++ b/src/greeter-broadcast.vala @@ -1,5 +1,6 @@ /* * Copyright 2014 © Canonical Ltd. + * Copyright 2021 © Robert Tari * * 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 @@ -15,6 +16,7 @@ * * Authors: * Ted Gould <ted@canonical.com> + * Robert Tari <robert@tari.in> */ [DBus (name = "org.ayatana.Desktop.Greeter.Broadcast")] @@ -22,9 +24,9 @@ public interface GreeterBroadcast : Object { // methods // unused public abstract async void RequestApplicationStart(string name, string appid) throws IOError; // unused public abstract async void RequestHomeShown(string name) throws IOError; - public abstract async void RequestSoundPlayPause(string name) throws IOError; - public abstract async void RequestSoundNext(string name) throws IOError; - public abstract async void RequestSoundPrev(string name) throws IOError; + public abstract async void RequestSoundPlayPause(string name) throws GLib.DBusError, GLib.IOError; + public abstract async void RequestSoundNext(string name) throws GLib.DBusError, GLib.IOError; + public abstract async void RequestSoundPrev(string name) throws GLib.DBusError, GLib.IOError; // signals // unused public signal void StartApplication(string username, string appid); // unused public signal void ShowHome(string username); diff --git a/src/media-player-list-greeter.vala b/src/media-player-list-greeter.vala index 6cd5c3f..766f17c 100644 --- a/src/media-player-list-greeter.vala +++ b/src/media-player-list-greeter.vala @@ -1,5 +1,6 @@ /* * Copyright © 2014 Canonical Ltd. + * Copyright © 2021 Robert Tari * * 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 @@ -15,109 +16,115 @@ * * Authors: * Ted Gould <ted@canonical.com> + * Robert Tari <robert@tari.in> */ [DBus (name="org.ayatana.Greeter.List")] public interface AyatanaGreeterList : Object { - public abstract async string get_active_entry () throws IOError; - public signal void entry_selected (string entry_name); + public abstract async string get_active_entry () throws GLib.DBusError, GLib.IOError; + public signal void entry_selected (string entry_name); } public class MediaPlayerListGreeter : MediaPlayerList { - string? selected_user = null; - AyatanaGreeterList? proxy = null; - HashTable<string, MediaPlayerUser> players = new HashTable<string, MediaPlayerUser>(str_hash, str_equal); - - public MediaPlayerListGreeter () { - Bus.get_proxy.begin<AyatanaGreeterList> ( - BusType.SESSION, - "org.ayatana.Greeter", - "/list", - DBusProxyFlags.NONE, - null, - new_proxy); - } - - void new_proxy (GLib.Object? obj, AsyncResult res) { - try { - this.proxy = Bus.get_proxy.end(res); - - this.proxy.entry_selected.connect(active_user_changed); - this.proxy.get_active_entry.begin ((obj, res) => { - try { - var value = (obj as AyatanaGreeterList).get_active_entry.end(res); - active_user_changed(value); - } catch (Error e) { - warning("Unable to get active entry: %s", e.message); - } - }); - } catch (Error e) { - this.proxy = null; - warning("Unable to create proxy to the greeter: %s", e.message); - } - } - - void active_user_changed (string active_user) { - /* No change, move along */ - if (selected_user == active_user) { - return; - } - - debug(@"Active user changed to: $active_user"); - - var old_user = selected_user; - - /* Protect against a null user */ - if (active_user != "" && active_user[0] != '*') { - selected_user = active_user; - } else { - debug(@"Blocking active user change for '$active_user'"); - selected_user = null; - } - - if (selected_user != null && !players.contains(selected_user)) { - players.insert(selected_user, new MediaPlayerUser(selected_user)); - } - - if (old_user != null) { - var old_player = players.lookup(old_user); - debug("Removing player for user: %s", old_user); - player_removed(old_player); - } - - if (selected_user != null) { - var new_player = players.lookup(selected_user); - - if (new_player != null) { - debug("Adding player for user: %s", selected_user); - player_added(new_player); - } - } - } - - /* We need to have an iterator for the interface, but eh, we can - only ever have one player for the current user */ - public class Iterator : MediaPlayerList.Iterator { - int i = 0; - MediaPlayerListGreeter list; - - public Iterator (MediaPlayerListGreeter in_list) { - list = in_list; - } - - public override MediaPlayer? next_value () { - MediaPlayer? retval = null; - - if (i == 0 && list.selected_user != null) { - retval = list.players.lookup(list.selected_user); - } - i++; - - return retval; - } - } - - public override MediaPlayerList.Iterator iterator() { - return new Iterator(this) as MediaPlayerList.Iterator; - } + string? selected_user = null; + AyatanaGreeterList? proxy = null; + HashTable<string, MediaPlayerUser> players = new HashTable<string, MediaPlayerUser>(str_hash, str_equal); + + public MediaPlayerListGreeter () { + Bus.get_proxy.begin<AyatanaGreeterList> ( + BusType.SESSION, + "org.ayatana.Greeter", + "/list", + DBusProxyFlags.NONE, + null, + new_proxy); + } + + void new_proxy (GLib.Object? obj, AsyncResult res) { + try { + this.proxy = Bus.get_proxy.end(res); + + this.proxy.entry_selected.connect(active_user_changed); + this.proxy.get_active_entry.begin ((obj, res) => { + try { + var list = (obj as AyatanaGreeterList); + + if (list != null) + { + var value = list.get_active_entry.end(res); + active_user_changed(value); + } + } catch (Error e) { + warning("Unable to get active entry: %s", e.message); + } + }); + } catch (Error e) { + this.proxy = null; + warning("Unable to create proxy to the greeter: %s", e.message); + } + } + + void active_user_changed (string active_user) { + /* No change, move along */ + if (selected_user == active_user) { + return; + } + + debug(@"Active user changed to: $active_user"); + + var old_user = selected_user; + + /* Protect against a null user */ + if (active_user != "" && active_user[0] != '*') { + selected_user = active_user; + } else { + debug(@"Blocking active user change for '$active_user'"); + selected_user = null; + } + + if (selected_user != null && !players.contains(selected_user)) { + players.insert(selected_user, new MediaPlayerUser(selected_user)); + } + + if (old_user != null) { + var old_player = players.lookup(old_user); + debug("Removing player for user: %s", old_user); + player_removed(old_player); + } + + if (selected_user != null) { + var new_player = players.lookup(selected_user); + + if (new_player != null) { + debug("Adding player for user: %s", selected_user); + player_added(new_player); + } + } + } + + /* We need to have an iterator for the interface, but eh, we can + only ever have one player for the current user */ + public class Iterator : MediaPlayerList.Iterator { + int i = 0; + MediaPlayerListGreeter list; + + public Iterator (MediaPlayerListGreeter in_list) { + list = in_list; + } + + public override MediaPlayer? next_value () { + MediaPlayer? retval = null; + + if (i == 0 && list.selected_user != null) { + retval = list.players.lookup(list.selected_user); + } + i++; + + return retval; + } + } + + public override MediaPlayerList.Iterator iterator() { + return new Iterator(this) as MediaPlayerList.Iterator; + } } diff --git a/src/media-player-list-mpris.vala b/src/media-player-list-mpris.vala index 65fb886..06bffc3 100644 --- a/src/media-player-list-mpris.vala +++ b/src/media-player-list-mpris.vala @@ -1,5 +1,6 @@ /* * Copyright 2013 Canonical Ltd. + * Copyright 2021 Robert Tari * * 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 @@ -15,6 +16,7 @@ * * Authors: * Lars Uebernickel <lars.uebernickel@canonical.com> + * Robert Tari <robert@tari.in> */ /** @@ -23,113 +25,113 @@ */ public class MediaPlayerListMpris : MediaPlayerList { - public MediaPlayerListMpris () { - this._players = new HashTable<string, MediaPlayerMpris> (str_hash, str_equal); - - BusWatcher.watch_namespace (BusType.SESSION, "org.mpris.MediaPlayer2", this.player_appeared, this.player_disappeared); - } - - /* only valid while the list is not changed */ - public class Iterator : MediaPlayerList.Iterator { - HashTableIter<string, MediaPlayerMpris> iter; - - public Iterator (MediaPlayerListMpris list) { - this.iter = HashTableIter<string, MediaPlayerMpris> (list._players); - } - - public override MediaPlayer? next_value () { - MediaPlayerMpris? player; - - if (this.iter.next (null, out player)) - return player as MediaPlayer; - else - return null; - } - } - - public override MediaPlayerList.Iterator iterator () { - return new Iterator (this) as MediaPlayerList.Iterator; - } - - /** - * Adds the player associated with @desktop_id. Does nothing if such a player already exists. - */ - MediaPlayerMpris? insert (string desktop_id) { - debug("Inserting player: %s", desktop_id); - - var id = desktop_id.has_suffix (".desktop") ? desktop_id : desktop_id + ".desktop"; - MediaPlayerMpris? player = this._players.lookup (id); - - if (player == null) { - var appinfo = new DesktopAppInfo (id); - if (appinfo == null) { - warning ("unable to find application '%s'", id); - return null; - } - - player = new MediaPlayerMpris (appinfo); - this._players.insert (player.id, player); - this.player_added (player); - } - - return player; - } - - /** - * Removes the player associated with @desktop_id, unless it is currently running. - */ - void remove (string desktop_id) { - MediaPlayer? player = this._players.lookup (desktop_id); - - if (player != null && !player.is_running) { - this._players.remove (desktop_id); - this.player_removed (player); - } - } - - /** - * Synchronizes the player list with @desktop_ids. After this call, this list will only contain the players - * in @desktop_ids. Players that were running but are not in @desktop_ids will remain in the list. - */ - public override void sync (string[] desktop_ids) { - - /* hash desktop_ids for faster lookup */ - var hash = new HashTable<string, unowned string> (str_hash, str_equal); - foreach (var id in desktop_ids) - hash.add (id); - - /* remove players that are not desktop_ids */ - foreach (var id in this._players.get_keys ()) { - if (!hash.contains (id)) - this.remove (id); - } - - /* insert all players (insert() takes care of not adding a player twice */ - foreach (var id in desktop_ids) - this.insert (id); - } - - HashTable<string, MediaPlayerMpris> _players; - - void player_appeared (DBusConnection connection, string name, string owner) { - try { - MprisRoot mpris2_root = Bus.get_proxy_sync (BusType.SESSION, name, MPRIS_MEDIA_PLAYER_PATH); - - var player = this.insert (mpris2_root.DesktopEntry); - if (player != null) - player.attach (mpris2_root, name); - } - catch (Error e) { - warning ("unable to create mpris proxy for '%s': %s", name, e.message); - } - } - - void player_disappeared (DBusConnection connection, string dbus_name) { - MediaPlayerMpris? player = this._players.find ( (name, player) => { - return player.dbus_name == dbus_name; - }); - - if (player != null) - player.detach (); - } + public MediaPlayerListMpris () { + this._players = new HashTable<string, MediaPlayerMpris> (str_hash, str_equal); + + BusWatcher.watch_namespace (BusType.SESSION, "org.mpris.MediaPlayer2", this.player_appeared, this.player_disappeared); + } + + /* only valid while the list is not changed */ + public class Iterator : MediaPlayerList.Iterator { + HashTableIter<string, MediaPlayerMpris> iter; + + public Iterator (MediaPlayerListMpris list) { + this.iter = HashTableIter<string, MediaPlayerMpris> (list._players); + } + + public override MediaPlayer? next_value () { + MediaPlayerMpris? player; + + if (this.iter.next (null, out player)) + return player as MediaPlayer; + else + return null; + } + } + + public override MediaPlayerList.Iterator iterator () { + return new Iterator (this) as MediaPlayerList.Iterator; + } + + /** + * Adds the player associated with @desktop_id. Does nothing if such a player already exists. + */ + MediaPlayerMpris? insert (string desktop_id) { + debug("Inserting player: %s", desktop_id); + + var id = desktop_id.has_suffix (".desktop") ? desktop_id : desktop_id + ".desktop"; + MediaPlayerMpris? player = this._players.lookup (id); + + if (player == null) { + var appinfo = new DesktopAppInfo (id); + if (appinfo == null) { + warning ("unable to find application '%s'", id); + return null; + } + + player = new MediaPlayerMpris (appinfo); + this._players.insert (player.id, player); + this.player_added (player); + } + + return player; + } + + /** + * Removes the player associated with @desktop_id, unless it is currently running. + */ + void remove (string desktop_id) { + MediaPlayer? player = this._players.lookup (desktop_id); + + if (player != null && !player.is_running) { + this._players.remove (desktop_id); + this.player_removed (player); + } + } + + /** + * Synchronizes the player list with @desktop_ids. After this call, this list will only contain the players + * in @desktop_ids. Players that were running but are not in @desktop_ids will remain in the list. + */ + public override void sync (string[] desktop_ids) { + + /* hash desktop_ids for faster lookup */ + var hash = new GenericSet<string> (str_hash, str_equal); + foreach (var id in desktop_ids) + hash.add (id); + + /* remove players that are not desktop_ids */ + foreach (var id in this._players.get_keys ()) { + if (!hash.contains (id)) + this.remove (id); + } + + /* insert all players (insert() takes care of not adding a player twice */ + foreach (var id in desktop_ids) + this.insert (id); + } + + HashTable<string, MediaPlayerMpris> _players; + + void player_appeared (DBusConnection connection, string name, string owner) { + try { + MprisRoot mpris2_root = Bus.get_proxy_sync (BusType.SESSION, name, MPRIS_MEDIA_PLAYER_PATH); + + var player = this.insert (mpris2_root.DesktopEntry); + if (player != null) + player.attach (mpris2_root, name); + } + catch (Error e) { + warning ("unable to create mpris proxy for '%s': %s", name, e.message); + } + } + + void player_disappeared (DBusConnection connection, string dbus_name) { + MediaPlayerMpris? player = this._players.find ( (name, player) => { + return player.dbus_name == dbus_name; + }); + + if (player != null) + player.detach (); + } } diff --git a/src/media-player-mpris.vala b/src/media-player-mpris.vala index 1b9dba5..fba004a 100644 --- a/src/media-player-mpris.vala +++ b/src/media-player-mpris.vala @@ -1,5 +1,6 @@ /* * Copyright 2013 Canonical Ltd. + * Copyright 2021 Robert Tari * * 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 @@ -15,6 +16,7 @@ * * Authors: * Lars Uebernickel <lars.uebernickel@canonical.com> + * Robert Tari <robert@tari.in> */ /** @@ -22,300 +24,300 @@ */ public class MediaPlayerMpris: MediaPlayer { - public MediaPlayerMpris (DesktopAppInfo appinfo) { - this.appinfo = appinfo; - } - - /** Desktop id of the player */ - public override string id { - get { - return this.appinfo.get_id (); - } - } - - /** Display name of the player */ - public override string name { - get { - return this.appinfo.get_name (); - } - } - - /** Application icon of the player */ - public override Icon? icon { - get { - return this.appinfo.get_icon (); - } - } - - /** - * True if an instance of the player is currently running. - * - * See also: attach(), detach() - */ - public override bool is_running { - get { - return this.proxy != null; - } - } - - /** Name of the player on the bus, if an instance is currently running */ - public override string dbus_name { - get { - return this._dbus_name; - } - } - - public override string state { - get; set; default = "Paused"; - } - - public override MediaPlayer.Track? current_track { - get; set; - } - - public override bool can_raise { - get { - return this.root != null ? this.root.CanRaise : true; - } - } - - public override bool can_do_play { - get { - return this.proxy.CanPlay; - } - } - - public override bool can_do_prev { - get { - return this.proxy.CanGoPrevious; - } - } - - public override bool can_do_next { - get { - return this.proxy.CanGoNext; - } - } - - /** - * Attach this object to a process of the associated media player. The player must own @dbus_name and - * implement the org.mpris.MediaPlayer2.Player interface. - * - * Only one player can be attached at any given time. Use detach() to detach a player. - * - * This method does not block. If it is successful, "is-running" will be set to %TRUE. - */ - public void attach (MprisRoot root, string dbus_name) { - return_if_fail (this._dbus_name == null && this.proxy == null); - - this.root = root; - this.notify_property ("can-raise"); - - this._dbus_name = dbus_name; - Bus.get_proxy.begin<MprisPlayer> (BusType.SESSION, dbus_name, "/org/mpris/MediaPlayer2", - DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, got_proxy); - Bus.get_proxy.begin<MprisPlaylists> (BusType.SESSION, dbus_name, "/org/mpris/MediaPlayer2", - DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, got_playlists_proxy); - } - - /** - * Detach this object from a process running the associated media player. - * - * See also: attach() - */ - public void detach () { - this.root = null; - this.proxy = null; - this._dbus_name = null; - this.notify_property ("is-running"); - this.notify_property ("can-raise"); - this.state = "Paused"; - this.current_track = null; - } - - /** - * Activate the associated media player. - * - * Note: this will _not_ call attach(), because it doesn't know on which dbus-name the player will appear. - * Use attach() to attach this object to a running instance of the player. - */ - public override void activate () { - try { - if (this.proxy == null) { - this.appinfo.launch (null, null); - this.state = "Launching"; - } - else if (this.root != null && this.root.CanRaise) { - this.root.Raise (); - } - } - catch (Error e) { - warning ("unable to activate %s: %s", appinfo.get_name (), e.message); - } - } - - /** - * Toggles playing status. - */ - public override void play_pause () { - if (this.proxy != null) { - this.proxy.PlayPause.begin (); - } - else if (this.state != "Launching") { - this.play_when_attached = true; - this.activate (); - } - } - - /** - * Skips to the next track. - */ - public override void next () { - if (this.proxy != null) - this.proxy.Next.begin (); - } - - /** - * Skips to the previous track. - */ - public override void previous () { - if (this.proxy != null) - this.proxy.Previous.begin (); - } - - public override uint get_n_playlists () { - return this.playlists != null ? this.playlists.length : 0; - } - - public override string get_playlist_id (int index) { - return_val_if_fail (index < this.playlists.length, ""); - return this.playlists[index].path; - } - - public override string get_playlist_name (int index) { - return_val_if_fail (index < this.playlists.length, ""); - return this.playlists[index].name; - } - - public override void activate_playlist_by_name (string name) { - if (this.playlists_proxy != null) - this.playlists_proxy.ActivatePlaylist.begin (new ObjectPath (name)); - } - - DesktopAppInfo appinfo; - MprisPlayer? proxy; - MprisPlaylists ?playlists_proxy; - string _dbus_name; - bool play_when_attached = false; - MprisRoot root; - PlaylistDetails[] playlists = null; - - void got_proxy (Object? obj, AsyncResult res) { - try { - this.proxy = Bus.get_proxy.end (res); - - /* Connecting to GDBusProxy's "g-properties-changed" signal here, because vala's dbus objects don't - * emit notify signals */ - var gproxy = this.proxy as DBusProxy; - gproxy.g_properties_changed.connect (this.proxy_properties_changed); - - this.notify_property ("is-running"); - this.state = this.proxy.PlaybackStatus != null ? this.proxy.PlaybackStatus : "Unknown"; - this.update_current_track (gproxy.get_cached_property ("Metadata")); - - if (this.play_when_attached) { - /* wait a little before calling PlayPause, some players need some time to - set themselves up */ - Timeout.add (1000, () => { proxy.PlayPause.begin (); return Source.REMOVE; } ); - this.play_when_attached = false; - } - } - catch (Error e) { - this._dbus_name = null; - warning ("unable to attach to media player: %s", e.message); - } - } - - void fetch_playlists () { - /* The proxy is created even when the interface is not supported. GDBusProxy will - return 0 for the PlaylistCount property in that case. */ - if (this.playlists_proxy != null && this.playlists_proxy.PlaylistCount > 0) { - this.playlists_proxy.GetPlaylists.begin (0, 100, "Alphabetical", false, (obj, res) => { - try { - this.playlists = playlists_proxy.GetPlaylists.end (res); - this.playlists_changed (); - } - catch (Error e) { - warning ("could not fetch playlists: %s", e.message); - this.playlists = null; - } - }); - } - else { - this.playlists = null; - this.playlists_changed (); - } - } - - void got_playlists_proxy (Object? obj, AsyncResult res) { - try { - this.playlists_proxy = Bus.get_proxy.end (res); - - var gproxy = this.proxy as DBusProxy; - gproxy.g_properties_changed.connect (this.playlists_proxy_properties_changed); - } - catch (Error e) { - warning ("unable to create mpris plalists proxy: %s", e.message); - return; - } - - Timeout.add (500, () => { this.fetch_playlists (); return Source.REMOVE; } ); - } - - /* some players (e.g. Spotify) don't follow the spec closely and pass single strings in metadata fields - * where an array of string is expected */ - static string sanitize_metadata_value (Variant? v) { - if (v == null) - return ""; - else if (v.is_of_type (VariantType.STRING)) - return v.get_string (); - else if (v.is_of_type (VariantType.STRING_ARRAY)) - return string.joinv (",", v.get_strv ()); - - warn_if_reached (); - return ""; - } - - void proxy_properties_changed (DBusProxy proxy, Variant changed_properties, string[] invalidated_properties) { - if (changed_properties.lookup ("PlaybackStatus", "s", null)) { - this.state = this.proxy.PlaybackStatus != null ? this.proxy.PlaybackStatus : "Unknown"; - } - if (changed_properties.lookup ("CanGoNext", "b", null) || changed_properties.lookup ("CanGoPrevious", "b", null) || + public MediaPlayerMpris (DesktopAppInfo appinfo) { + this.appinfo = appinfo; + } + + /** Desktop id of the player */ + public override string id { + get { + return this.appinfo.get_id (); + } + } + + /** Display name of the player */ + public override string name { + get { + return this.appinfo.get_name (); + } + } + + /** Application icon of the player */ + public override Icon? icon { + get { + return this.appinfo.get_icon (); + } + } + + /** + * True if an instance of the player is currently running. + * + * See also: attach(), detach() + */ + public override bool is_running { + get { + return this.proxy != null; + } + } + + /** Name of the player on the bus, if an instance is currently running */ + public override string dbus_name { + get { + return this._dbus_name; + } + } + + public override string state { + get; set; default = "Paused"; + } + + public override MediaPlayer.Track? current_track { + get; set; + } + + public override bool can_raise { + get { + return this.root != null ? this.root.CanRaise : true; + } + } + + public override bool can_do_play { + get { + return this.proxy.CanPlay; + } + } + + public override bool can_do_prev { + get { + return this.proxy.CanGoPrevious; + } + } + + public override bool can_do_next { + get { + return this.proxy.CanGoNext; + } + } + + /** + * Attach this object to a process of the associated media player. The player must own @dbus_name and + * implement the org.mpris.MediaPlayer2.Player interface. + * + * Only one player can be attached at any given time. Use detach() to detach a player. + * + * This method does not block. If it is successful, "is-running" will be set to %TRUE. + */ + public void attach (MprisRoot root, string dbus_name) { + return_if_fail (this._dbus_name == null && this.proxy == null); + + this.root = root; + this.notify_property ("can-raise"); + + this._dbus_name = dbus_name; + Bus.get_proxy.begin<MprisPlayer> (BusType.SESSION, dbus_name, "/org/mpris/MediaPlayer2", + DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, got_proxy); + Bus.get_proxy.begin<MprisPlaylists> (BusType.SESSION, dbus_name, "/org/mpris/MediaPlayer2", + DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, got_playlists_proxy); + } + + /** + * Detach this object from a process running the associated media player. + * + * See also: attach() + */ + public void detach () { + this.root = null; + this.proxy = null; + this._dbus_name = null; + this.notify_property ("is-running"); + this.notify_property ("can-raise"); + this.state = "Paused"; + this.current_track = null; + } + + /** + * Activate the associated media player. + * + * Note: this will _not_ call attach(), because it doesn't know on which dbus-name the player will appear. + * Use attach() to attach this object to a running instance of the player. + */ + public override void activate () { + try { + if (this.proxy == null) { + this.appinfo.launch (null, null); + this.state = "Launching"; + } + else if (this.root != null && this.root.CanRaise) { + this.root.Raise.begin (); + } + } + catch (Error e) { + warning ("unable to activate %s: %s", appinfo.get_name (), e.message); + } + } + + /** + * Toggles playing status. + */ + public override void play_pause () { + if (this.proxy != null) { + this.proxy.PlayPause.begin (); + } + else if (this.state != "Launching") { + this.play_when_attached = true; + this.activate (); + } + } + + /** + * Skips to the next track. + */ + public override void next () { + if (this.proxy != null) + this.proxy.Next.begin (); + } + + /** + * Skips to the previous track. + */ + public override void previous () { + if (this.proxy != null) + this.proxy.Previous.begin (); + } + + public override uint get_n_playlists () { + return this.playlists != null ? this.playlists.length : 0; + } + + public override string get_playlist_id (int index) { + return_val_if_fail (index < this.playlists.length, ""); + return this.playlists[index].path; + } + + public override string get_playlist_name (int index) { + return_val_if_fail (index < this.playlists.length, ""); + return this.playlists[index].name; + } + + public override void activate_playlist_by_name (string name) { + if (this.playlists_proxy != null) + this.playlists_proxy.ActivatePlaylist.begin (new ObjectPath (name)); + } + + DesktopAppInfo appinfo; + MprisPlayer? proxy; + MprisPlaylists ?playlists_proxy; + string _dbus_name; + bool play_when_attached = false; + MprisRoot root; + PlaylistDetails[] playlists = null; + + void got_proxy (Object? obj, AsyncResult res) { + try { + this.proxy = Bus.get_proxy.end (res); + + /* Connecting to GDBusProxy's "g-properties-changed" signal here, because vala's dbus objects don't + * emit notify signals */ + var gproxy = this.proxy as DBusProxy; + gproxy.g_properties_changed.connect (this.proxy_properties_changed); + + this.notify_property ("is-running"); + this.state = this.proxy.PlaybackStatus != null ? this.proxy.PlaybackStatus : "Unknown"; + this.update_current_track (gproxy.get_cached_property ("Metadata")); + + if (this.play_when_attached) { + /* wait a little before calling PlayPause, some players need some time to + set themselves up */ + Timeout.add (1000, () => { proxy.PlayPause.begin (); return Source.REMOVE; } ); + this.play_when_attached = false; + } + } + catch (Error e) { + this._dbus_name = null; + warning ("unable to attach to media player: %s", e.message); + } + } + + void fetch_playlists () { + /* The proxy is created even when the interface is not supported. GDBusProxy will + return 0 for the PlaylistCount property in that case. */ + if (this.playlists_proxy != null && this.playlists_proxy.PlaylistCount > 0) { + this.playlists_proxy.GetPlaylists.begin (0, 100, "Alphabetical", false, (obj, res) => { + try { + this.playlists = playlists_proxy.GetPlaylists.end (res); + this.playlists_changed (); + } + catch (Error e) { + warning ("could not fetch playlists: %s", e.message); + this.playlists = null; + } + }); + } + else { + this.playlists = null; + this.playlists_changed (); + } + } + + void got_playlists_proxy (Object? obj, AsyncResult res) { + try { + this.playlists_proxy = Bus.get_proxy.end (res); + + var gproxy = this.proxy as DBusProxy; + gproxy.g_properties_changed.connect (this.playlists_proxy_properties_changed); + } + catch (Error e) { + warning ("unable to create mpris plalists proxy: %s", e.message); + return; + } + + Timeout.add (500, () => { this.fetch_playlists (); return Source.REMOVE; } ); + } + + /* some players (e.g. Spotify) don't follow the spec closely and pass single strings in metadata fields + * where an array of string is expected */ + static string sanitize_metadata_value (Variant? v) { + if (v == null) + return ""; + else if (v.is_of_type (VariantType.STRING)) + return v.get_string (); + else if (v.is_of_type (VariantType.STRING_ARRAY)) + return string.joinv (",", v.get_strv ()); + + warn_if_reached (); + return ""; + } + + void proxy_properties_changed (DBusProxy proxy, Variant changed_properties, string[] invalidated_properties) { + if (changed_properties.lookup ("PlaybackStatus", "s", null)) { + this.state = this.proxy.PlaybackStatus != null ? this.proxy.PlaybackStatus : "Unknown"; + } + if (changed_properties.lookup ("CanGoNext", "b", null) || changed_properties.lookup ("CanGoPrevious", "b", null) || changed_properties.lookup ("CanPlay", "b", null) || changed_properties.lookup ("CanPause", "b", null)) { - this.playbackstatus_changed (); - } - - var metadata = changed_properties.lookup_value ("Metadata", VariantType.VARDICT); - if (metadata != null) - this.update_current_track (metadata); - } - - void playlists_proxy_properties_changed (DBusProxy proxy, Variant changed_properties, string[] invalidated_properties) { - if (changed_properties.lookup ("PlaylistCount", "u", null)) - this.fetch_playlists (); - } - - void update_current_track (Variant? metadata) { - if (metadata != null) { - this.current_track = new Track ( - sanitize_metadata_value (metadata.lookup_value ("xesam:artist", null)), - sanitize_metadata_value (metadata.lookup_value ("xesam:title", null)), - sanitize_metadata_value (metadata.lookup_value ("xesam:album", null)), - sanitize_metadata_value (metadata.lookup_value ("mpris:artUrl", null)) - ); - } - else { - this.current_track = null; - } - } + this.playbackstatus_changed (); + } + + var metadata = changed_properties.lookup_value ("Metadata", VariantType.VARDICT); + if (metadata != null) + this.update_current_track (metadata); + } + + void playlists_proxy_properties_changed (DBusProxy proxy, Variant changed_properties, string[] invalidated_properties) { + if (changed_properties.lookup ("PlaylistCount", "u", null)) + this.fetch_playlists (); + } + + void update_current_track (Variant? metadata) { + if (metadata != null) { + this.current_track = new Track ( + sanitize_metadata_value (metadata.lookup_value ("xesam:artist", null)), + sanitize_metadata_value (metadata.lookup_value ("xesam:title", null)), + sanitize_metadata_value (metadata.lookup_value ("xesam:album", null)), + sanitize_metadata_value (metadata.lookup_value ("mpris:artUrl", null)) + ); + } + else { + this.current_track = null; + } + } } diff --git a/src/media-player-user.vala b/src/media-player-user.vala index 0071b93..64e9bd5 100644 --- a/src/media-player-user.vala +++ b/src/media-player-user.vala @@ -1,5 +1,6 @@ /* * Copyright © 2014 Canonical Ltd. + * Copyright © 2021 Robert Tari * * 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 @@ -15,280 +16,296 @@ * * Authors: * Ted Gould <ted@canonical.com> + * Robert Tari <robert@tari.in> */ public class MediaPlayerUser : MediaPlayer { - Act.UserManager accounts_manager = Act.UserManager.get_default(); - string username; - Act.User? actuser = null; - AccountsServiceSoundSettings? proxy = null; - GreeterBroadcast? greeter = null; - - HashTable<string, bool> properties_queued = new HashTable<string, bool>(str_hash, str_equal); - uint properties_timeout = 0; - - /* Grab the user from the Accounts service and, when it is loaded then - set up a proxy to its sound settings */ - public MediaPlayerUser(string user) { - username = user; - - actuser = accounts_manager.get_user(user); - actuser.notify["is-loaded"].connect(() => { - debug("User loaded"); - - this.proxy = null; - - Bus.get_proxy.begin<AccountsServiceSoundSettings> ( - BusType.SYSTEM, - "org.freedesktop.Accounts", - actuser.get_object_path(), - DBusProxyFlags.GET_INVALIDATED_PROPERTIES, - null, - new_proxy); - }); - - Bus.get_proxy.begin<GreeterBroadcast> ( - BusType.SYSTEM, - "org.ayatana.Desktop.Greeter.Broadcast", - "/org/ayatana/Desktop/Greeter/Broadcast", - DBusProxyFlags.NONE, - null, - greeter_proxy_new); - } - - ~MediaPlayerUser () { - if (properties_timeout != 0) { - Source.remove(properties_timeout); - properties_timeout = 0; - } - } - - /* Ensure that we've collected all the changes so that we only signal - once for variables like 'track' */ - bool properties_idle () { - properties_timeout = 0; - - properties_queued.@foreach((key, value) => { - debug("Notifying '%s' changed", key); - this.notify_property(key); - }); - - properties_queued.remove_all(); - - return Source.REMOVE; - } - - /* Turns the DBus names into the object properties */ - void queue_property_notification (string dbus_property_name) { - if (properties_timeout == 0) { - properties_timeout = Idle.add(properties_idle); - } - - switch (dbus_property_name) { - case "Timestamp": - properties_queued.insert("name", true); - properties_queued.insert("icon", true); - properties_queued.insert("state", true); - properties_queued.insert("current-track", true); - properties_queued.insert("is-running", true); - break; - case "PlayerName": - properties_queued.insert("name", true); - break; - case "PlayerIcon": - properties_queued.insert("icon", true); - break; - case "State": - properties_queued.insert("state", true); - break; - case "Title": - case "Artist": - case "Album": - case "ArtUrl": - properties_queued.insert("current-track", true); - break; - } - } - - void new_proxy (GLib.Object? obj, AsyncResult res) { - try { - this.proxy = Bus.get_proxy.end (res); - - var gproxy = this.proxy as DBusProxy; - gproxy.g_properties_changed.connect ((proxy, changed, invalidated) => { - string key = ""; - Variant value; - VariantIter iter = new VariantIter(changed); - - while (iter.next("{sv}", &key, &value)) { - queue_property_notification(key); - } - - foreach (var invalid in invalidated) { - queue_property_notification(invalid); - } - }); - - debug("Notifying player is ready for user: %s", this.username); - this.notify_property("is-running"); - } catch (Error e) { - this.proxy = null; - warning("Unable to get proxy to user '%s' sound settings: %s", username, e.message); - } - } - - bool proxy_is_valid () { - if (this.proxy == null) { - return false; - } - - /* More than 10 minutes old */ - if (this.proxy.timestamp < GLib.get_monotonic_time() - 10 * 60 * 1000 * 1000) { - return false; - } - - return true; - } - - public override string id { - get { return username; } - } - - /* These values come from the proxy */ - string name_cache; - public override string name { - get { - if (proxy_is_valid()) { - name_cache = this.proxy.player_name; - debug("Player Name: %s", name_cache); - return name_cache; - } else { - return ""; - } - } - } - string state_cache; - public override string state { - get { - if (proxy_is_valid()) { - state_cache = this.proxy.state; - debug("State: %s", state_cache); - return state_cache; - } else { - return ""; - } - } - set { } - } - Icon icon_cache; - public override Icon? icon { - get { - if (proxy_is_valid()) { - icon_cache = Icon.deserialize(this.proxy.player_icon); - return icon_cache; - } else { - return null; - } - } - } - - /* Placeholder */ - public override string dbus_name { get { return ""; } } - - /* If it's shown externally it's running */ - public override bool is_running { get { return proxy_is_valid(); } } - /* A bit weird. Not sure how we should handle this. */ - public override bool can_raise { get { return true; } } - - /* Fill out the track based on the values in the proxy */ - MediaPlayer.Track track_cache; - public override MediaPlayer.Track? current_track { - get { - if (proxy_is_valid()) { - track_cache = new MediaPlayer.Track( - this.proxy.artist, - this.proxy.title, - this.proxy.album, - this.proxy.art_url - ); - return track_cache; - } else { - return null; - } - } - set { } - } - - void greeter_proxy_new (GLib.Object? obj, AsyncResult res) { - try { - this.greeter = Bus.get_proxy.end (res); - } catch (Error e) { - this.greeter = null; - warning("Unable to get greeter proxy: %s", e.message); - } - } - - /* Control functions through unity-greeter-session-broadcast */ - public override void activate () { - /* TODO: */ - } - public override void play_pause () { - debug("Play Pause for user: %s", this.username); - - if (this.greeter != null) { - this.greeter.RequestSoundPlayPause.begin(this.username, (obj, res) => { - try { - (obj as GreeterBroadcast).RequestSoundPlayPause.end(res); - } catch (Error e) { - warning("Unable to send play pause: %s", e.message); - } - }); - } else { - warning("No unity-greeter-session-broadcast to send play-pause"); - } - } - public override void next () { - debug("Next for user: %s", this.username); - - if (this.greeter != null) { - this.greeter.RequestSoundNext.begin(this.username, (obj, res) => { - try { - (obj as GreeterBroadcast).RequestSoundNext.end(res); - } catch (Error e) { - warning("Unable to send next: %s", e.message); - } - }); - } else { - warning("No unity-greeter-session-broadcast to send next"); - } - } - public override void previous () { - debug("Previous for user: %s", this.username); - - if (this.greeter != null) { - this.greeter.RequestSoundPrev.begin(this.username, (obj, res) => { - try { - (obj as GreeterBroadcast).RequestSoundPrev.end(res); - } catch (Error e) { - warning("Unable to send previous: %s", e.message); - } - }); - } else { - warning("No unity-greeter-session-broadcast to send previous"); - } - } - - /* Play list functions are all null as we don't support the - playlist feature on the greeter */ - public override uint get_n_playlists() { - return 0; - } - public override string get_playlist_id (int index) { - return ""; - } - public override string get_playlist_name (int index) { - return ""; - } - public override void activate_playlist_by_name (string playlist) { - } + Act.UserManager accounts_manager = Act.UserManager.get_default(); + string username; + Act.User? actuser = null; + AccountsServiceSoundSettings? proxy = null; + GreeterBroadcast? greeter = null; + + HashTable<string, bool> properties_queued = new HashTable<string, bool>(str_hash, str_equal); + uint properties_timeout = 0; + + /* Grab the user from the Accounts service and, when it is loaded then + set up a proxy to its sound settings */ + public MediaPlayerUser(string user) { + username = user; + + actuser = accounts_manager.get_user(user); + actuser.notify["is-loaded"].connect(() => { + debug("User loaded"); + + this.proxy = null; + + Bus.get_proxy.begin<AccountsServiceSoundSettings> ( + BusType.SYSTEM, + "org.freedesktop.Accounts", + actuser.get_object_path(), + DBusProxyFlags.GET_INVALIDATED_PROPERTIES, + null, + new_proxy); + }); + + Bus.get_proxy.begin<GreeterBroadcast> ( + BusType.SYSTEM, + "org.ayatana.Desktop.Greeter.Broadcast", + "/org/ayatana/Desktop/Greeter/Broadcast", + DBusProxyFlags.NONE, + null, + greeter_proxy_new); + } + + ~MediaPlayerUser () { + if (properties_timeout != 0) { + Source.remove(properties_timeout); + properties_timeout = 0; + } + } + + /* Ensure that we've collected all the changes so that we only signal + once for variables like 'track' */ + bool properties_idle () { + properties_timeout = 0; + + properties_queued.@foreach((key, value) => { + debug("Notifying '%s' changed", key); + this.notify_property(key); + }); + + properties_queued.remove_all(); + + return Source.REMOVE; + } + + /* Turns the DBus names into the object properties */ + void queue_property_notification (string dbus_property_name) { + if (properties_timeout == 0) { + properties_timeout = Idle.add(properties_idle); + } + + switch (dbus_property_name) { + case "Timestamp": + properties_queued.insert("name", true); + properties_queued.insert("icon", true); + properties_queued.insert("state", true); + properties_queued.insert("current-track", true); + properties_queued.insert("is-running", true); + break; + case "PlayerName": + properties_queued.insert("name", true); + break; + case "PlayerIcon": + properties_queued.insert("icon", true); + break; + case "State": + properties_queued.insert("state", true); + break; + case "Title": + case "Artist": + case "Album": + case "ArtUrl": + properties_queued.insert("current-track", true); + break; + } + } + + void new_proxy (GLib.Object? obj, AsyncResult res) { + try { + this.proxy = Bus.get_proxy.end (res); + + var gproxy = this.proxy as DBusProxy; + gproxy.g_properties_changed.connect ((proxy, changed, invalidated) => { + string key = ""; + Variant value; + VariantIter iter = new VariantIter(changed); + + while (iter.next("{sv}", &key, &value)) { + queue_property_notification(key); + } + + foreach (var invalid in invalidated) { + queue_property_notification(invalid); + } + }); + + debug("Notifying player is ready for user: %s", this.username); + this.notify_property("is-running"); + } catch (Error e) { + this.proxy = null; + warning("Unable to get proxy to user '%s' sound settings: %s", username, e.message); + } + } + + bool proxy_is_valid () { + if (this.proxy == null) { + return false; + } + + /* More than 10 minutes old */ + if (this.proxy.timestamp < GLib.get_monotonic_time() - 10 * 60 * 1000 * 1000) { + return false; + } + + return true; + } + + public override string id { + get { return username; } + } + + /* These values come from the proxy */ + string name_cache; + public override string name { + get { + if (proxy_is_valid()) { + name_cache = this.proxy.player_name; + debug("Player Name: %s", name_cache); + return name_cache; + } else { + return ""; + } + } + } + string state_cache; + public override string state { + get { + if (proxy_is_valid()) { + state_cache = this.proxy.state; + debug("State: %s", state_cache); + return state_cache; + } else { + return ""; + } + } + set { } + } + Icon icon_cache; + public override Icon? icon { + get { + if (proxy_is_valid()) { + icon_cache = Icon.deserialize(this.proxy.player_icon); + return icon_cache; + } else { + return null; + } + } + } + + /* Placeholder */ + public override string dbus_name { get { return ""; } } + + /* If it's shown externally it's running */ + public override bool is_running { get { return proxy_is_valid(); } } + /* A bit weird. Not sure how we should handle this. */ + public override bool can_raise { get { return true; } } + + /* Fill out the track based on the values in the proxy */ + MediaPlayer.Track track_cache; + public override MediaPlayer.Track? current_track { + get { + if (proxy_is_valid()) { + track_cache = new MediaPlayer.Track( + this.proxy.artist, + this.proxy.title, + this.proxy.album, + this.proxy.art_url + ); + return track_cache; + } else { + return null; + } + } + set { } + } + + void greeter_proxy_new (GLib.Object? obj, AsyncResult res) { + try { + this.greeter = Bus.get_proxy.end (res); + } catch (Error e) { + this.greeter = null; + warning("Unable to get greeter proxy: %s", e.message); + } + } + + /* Control functions through unity-greeter-session-broadcast */ + public override void activate () { + /* TODO: */ + } + public override void play_pause () { + debug("Play Pause for user: %s", this.username); + + if (this.greeter != null) { + this.greeter.RequestSoundPlayPause.begin(this.username, (obj, res) => { + try { + var broadcasts = (obj as GreeterBroadcast); + + if (broadcasts != null) + { + broadcasts.RequestSoundPlayPause.end(res); + } + } catch (Error e) { + warning("Unable to send play pause: %s", e.message); + } + }); + } else { + warning("No unity-greeter-session-broadcast to send play-pause"); + } + } + public override void next () { + debug("Next for user: %s", this.username); + + if (this.greeter != null) { + this.greeter.RequestSoundNext.begin(this.username, (obj, res) => { + try { + var broadcasts = (obj as GreeterBroadcast); + + if (broadcasts != null) + { + broadcasts.RequestSoundNext.end(res); + } + } catch (Error e) { + warning("Unable to send next: %s", e.message); + } + }); + } else { + warning("No unity-greeter-session-broadcast to send next"); + } + } + public override void previous () { + debug("Previous for user: %s", this.username); + + if (this.greeter != null) { + this.greeter.RequestSoundPrev.begin(this.username, (obj, res) => { + try { + var broadcasts = (obj as GreeterBroadcast); + + if (broadcasts != null) + { + broadcasts.RequestSoundPrev.end(res); + } + } catch (Error e) { + warning("Unable to send previous: %s", e.message); + } + }); + } else { + warning("No unity-greeter-session-broadcast to send previous"); + } + } + + /* Play list functions are all null as we don't support the + playlist feature on the greeter */ + public override uint get_n_playlists() { + return 0; + } + public override string get_playlist_id (int index) { + return ""; + } + public override string get_playlist_name (int index) { + return ""; + } + public override void activate_playlist_by_name (string playlist) { + } } diff --git a/src/mpris2-interfaces.vala b/src/mpris2-interfaces.vala index f9060af..7ab641f 100644 --- a/src/mpris2-interfaces.vala +++ b/src/mpris2-interfaces.vala @@ -1,7 +1,10 @@ /* Copyright 2010-2015 Canonical Ltd. +Copyright 2021 Robert Tari + Authors: Conor Curran <conor.curran@canonical.com> + Robert Tari <robert@tari.in> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published @@ -28,8 +31,8 @@ public interface MprisRoot : Object { public abstract string Identity{owned get; set;} public abstract string DesktopEntry{owned get; set;} // methods - public abstract async void Quit() throws IOError; - public abstract async void Raise() throws IOError; + public abstract async void Quit() throws GLib.DBusError, GLib.IOError; + public abstract async void Raise() throws GLib.DBusError, GLib.IOError; } [DBus (name = "org.mpris.MediaPlayer2.Player")] @@ -42,10 +45,10 @@ public interface MprisPlayer : Object { public abstract bool CanGoNext{owned get; set;} public abstract bool CanGoPrevious{owned get; set;} // methods - public abstract async void PlayPause() throws IOError; - public abstract async void Next() throws IOError; - public abstract async void Previous() throws IOError; - public abstract async void Seek(int64 offset) throws IOError; + public abstract async void PlayPause() throws GLib.DBusError, GLib.IOError; + public abstract async void Next() throws GLib.DBusError, GLib.IOError; + public abstract async void Previous() throws GLib.DBusError, GLib.IOError; + public abstract async void Seek(int64 offset) throws GLib.DBusError, GLib.IOError; // signals public signal void Seeked(int64 new_position); } @@ -69,14 +72,14 @@ public interface MprisPlaylists : Object { public abstract string[] Orderings{owned get; set;} public abstract uint32 PlaylistCount{owned get; set;} public abstract ActivePlaylistContainer? ActivePlaylist {owned get; set;} - + //methods - public abstract async void ActivatePlaylist(ObjectPath playlist_id) throws IOError; + public abstract async void ActivatePlaylist(ObjectPath playlist_id) throws GLib.DBusError, GLib.IOError; public abstract async PlaylistDetails[]? GetPlaylists ( uint32 index, uint32 max_count, string order, - bool reverse_order ) throws IOError; + bool reverse_order ) throws GLib.DBusError, GLib.IOError; //signals public signal void PlaylistChanged (PlaylistDetails details); - + } diff --git a/src/sound-menu.vala b/src/sound-menu.vala index 957238c..bcf2042 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -1,5 +1,6 @@ /* * Copyright 2013 Canonical Ltd. + * Copyright 2021 Robert Tari * * 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 @@ -15,6 +16,7 @@ * * Authors: * Lars Uebernickel <lars.uebernickel@canonical.com> + * Robert Tari <robert@tari.in> */ public class SoundMenu: Object @@ -271,6 +273,8 @@ public class SoundMenu: Object case VolumeControl.ActiveOutput.HDMI_HEADPHONES: label = _("Volume (HDMI headphones)"); break; + case VolumeControl.ActiveOutput.CALL_MODE: + break; } this.volume_section.remove (index); this.volume_section.insert_item (index, this.create_slider_menu_item (_(label), "indicator.volume(0)", 0.0, 1.0, 0.01, diff --git a/src/volume-control-pulse.vala b/src/volume-control-pulse.vala index e524c2e..e94ef75 100644 --- a/src/volume-control-pulse.vala +++ b/src/volume-control-pulse.vala @@ -1,6 +1,7 @@ /* * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*- * Copyright 2013 Canonical Ltd. + * Copyright 2021 Robert Tari * * 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 @@ -16,6 +17,7 @@ * * Authors: * Alberto Ruiz <alberto.ruiz@canonical.com> + * Robert Tari <robert@tari.in> */ using PulseAudio; @@ -24,841 +26,846 @@ using Gee; public class VolumeControlPulse : VolumeControl { - private unowned PulseAudio.GLibMainLoop loop = null; - - private uint _reconnect_timer = 0; - - private PulseAudio.Context context; - private bool _mute = true; - private VolumeControl.Volume _volume = new VolumeControl.Volume(); - private double _mic_volume = 0.0; - - /* Used by the pulseaudio stream restore extension */ - private DBusConnection _pconn; - /* Need both the list and hash so we can retrieve the last known sink-input after - * releasing the current active one (restoring back to the previous known role) */ - private Gee.ArrayList<uint32> _sink_input_list = new Gee.ArrayList<uint32> (); - private HashMap<uint32, string> _sink_input_hash = new HashMap<uint32, string> (); - private bool _pulse_use_stream_restore = false; - private int32 _active_sink_input = -1; - private string[] _valid_roles = {"multimedia", "alert", "alarm", "phone"}; - private string? _objp_role_multimedia = null; - private string? _objp_role_alert = null; - private string? _objp_role_alarm = null; - private string? _objp_role_phone = null; - private uint _pa_volume_sig_count = 0; - - private uint _local_volume_timer = 0; - private uint _accountservice_volume_timer = 0; - private bool _send_next_local_volume = false; - private double _account_service_volume = 0.0; - private VolumeControl.ActiveOutput _active_output = VolumeControl.ActiveOutput.SPEAKERS; - private AccountsServiceAccess _accounts_service_access; - private bool _external_mic_detected = false; - private bool _source_sink_mic_activated = false; - - /** true when a microphone is active **/ - public override bool active_mic { get; set; default = false; } - - public VolumeControlPulse (IndicatorSound.Options options, PulseAudio.GLibMainLoop loop, AccountsServiceAccess? accounts_service_access) - { - base(options); - - _volume.volume = 0.0; - _volume.reason = VolumeControl.VolumeReasons.PULSE_CHANGE; - - this.loop = loop; - - _accounts_service_access = accounts_service_access; - this._accounts_service_access.notify["volume"].connect(() => { - if (this._accounts_service_access.volume >= 0 && _account_service_volume != this._accounts_service_access.volume) { - _account_service_volume = this._accounts_service_access.volume; - // we need to wait for this to settle. - start_account_service_volume_timer(); - } - }); - this.reconnect_to_pulse (); - } - - ~VolumeControlPulse () - { - stop_all_timers(); - } - - private void stop_all_timers() - { - if (_reconnect_timer != 0) { - Source.remove (_reconnect_timer); - _reconnect_timer = 0; - } - stop_local_volume_timer(); - stop_account_service_volume_timer(); - } - - public static VolumeControl.ActiveOutput calculate_active_output (SinkInfo? sink) { - - VolumeControl.ActiveOutput ret_output = VolumeControl.ActiveOutput.SPEAKERS; - /* Check if the current active port is headset/headphone */ - /* There is not easy way to check if the port is a headset/headphone besides - * checking for the port name. On touch (with the pulseaudio droid element) - * the headset/headphone port is called 'output-headset' and 'output-headphone'. - * On the desktop this is usually called 'analog-output-headphones' */ - - // first of all check if we are in call mode - if (sink.active_port != null && sink.active_port.name == "output-speaker+wired_headphone") { - return VolumeControl.ActiveOutput.CALL_MODE; - } - // look if it's a headset/headphones - if (sink.name == "indicator_sound_test_headphones" || - (sink.active_port != null && - (sink.active_port.name.contains("headset") || - sink.active_port.name.contains("headphone")))) { - // check if it's a bluetooth device - unowned string device_bus = sink.proplist.gets ("device.bus"); - if (device_bus != null && device_bus == "bluetooth") { - ret_output = VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES; - } else if (device_bus != null && device_bus == "usb") { - ret_output = VolumeControl.ActiveOutput.USB_HEADPHONES; - } else if (device_bus != null && device_bus == "hdmi") { - ret_output = VolumeControl.ActiveOutput.HDMI_HEADPHONES; - } else { - ret_output = VolumeControl.ActiveOutput.HEADPHONES; - } - } else { - // speaker - unowned string device_bus = sink.proplist.gets ("device.bus"); - if (device_bus != null && device_bus == "bluetooth") { - ret_output = VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER; - } else if (device_bus != null && device_bus == "usb") { - ret_output = VolumeControl.ActiveOutput.USB_SPEAKER; - } else if (device_bus != null && device_bus == "hdmi") { - ret_output = VolumeControl.ActiveOutput.HDMI_SPEAKER; - } else { - ret_output = VolumeControl.ActiveOutput.SPEAKERS; - } - } - - return ret_output; - } - - private bool is_external_mic (SourceInfo? sink) { - if (sink.name.contains ("indicator_sound_test_mic")) { - return true; - } - if (sink.active_port != null && - ( (sink.active_port.name.contains ("headphone") || - sink.active_port.name.contains ("headset") || - sink.active_port.name.contains ("mic") ) && - (!sink.active_port.name.contains ("internal") && - !sink.active_port.name.contains ("builtin")) )) { - return true; - } - return false; - } - - - /* PulseAudio logic*/ - private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index) - { - switch (t & Context.SubscriptionEventType.FACILITY_MASK) - { - case Context.SubscriptionEventType.SINK: - update_sink (); - break; - - case Context.SubscriptionEventType.SINK_INPUT: - switch (t & Context.SubscriptionEventType.TYPE_MASK) - { - case Context.SubscriptionEventType.NEW: - c.get_sink_input_info (index, handle_new_sink_input_cb); - break; - - case Context.SubscriptionEventType.CHANGE: - c.get_sink_input_info (index, handle_changed_sink_input_cb); - break; - - case Context.SubscriptionEventType.REMOVE: - remove_sink_input_from_list (index); - break; - default: - debug ("Sink input event not known."); - break; - } - break; - - case Context.SubscriptionEventType.SOURCE: - update_source (); - break; - - case Context.SubscriptionEventType.SOURCE_OUTPUT: - switch (t & Context.SubscriptionEventType.TYPE_MASK) - { - case Context.SubscriptionEventType.NEW: - c.get_source_output_info (index, source_output_info_cb); - break; - - case Context.SubscriptionEventType.REMOVE: - this._source_sink_mic_activated = false; - this.active_mic = _external_mic_detected; - break; - } - break; - } - } - - private void sink_info_cb_for_props (Context c, SinkInfo? i, int eol) - { - if (i == null) - return; - - if (_mute != (bool)i.mute) - { - _mute = (bool)i.mute; - this.notify_property ("mute"); - } - - var playing = (i.state == PulseAudio.SinkState.RUNNING); - if (is_playing != playing) - is_playing = playing; - - var oldval = _active_output; - var newval = calculate_active_output(i); - - _active_output = newval; - - // Emit a change signal iff CALL_MODE wasn't involved. (FIXME: yuck.) - if ((oldval != VolumeControl.ActiveOutput.CALL_MODE) && - (newval != VolumeControl.ActiveOutput.CALL_MODE) && - (oldval != newval)) { - this.active_output_changed (newval); - } - - if (_pulse_use_stream_restore == false && - _volume.volume != volume_to_double (i.volume.max ())) - { - var vol = new VolumeControl.Volume(); - vol.volume = volume_to_double (i.volume.max ()); - vol.reason = VolumeControl.VolumeReasons.PULSE_CHANGE; - this.volume = vol; - } - } - - private void source_info_cb (Context c, SourceInfo? i, int eol) - { - if (i == null) - return; - - if (is_external_mic (i)) { - this.active_mic = true; - _external_mic_detected = true; - } else { - this.active_mic = _source_sink_mic_activated; - _external_mic_detected = false; - } - - if (_mic_volume != volume_to_double (i.volume.values[0])) - { - _mic_volume = volume_to_double (i.volume.values[0]); - this.notify_property ("mic-volume"); - } - } - - private void server_info_cb_for_props (Context c, ServerInfo? i) - { - if (i == null) - return; - context.get_sink_info_by_name (i.default_sink_name, sink_info_cb_for_props); - } - - private void update_sink () - { - context.get_server_info (server_info_cb_for_props); - } - - private void update_source_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) { - if (i != null) - context.get_source_info_by_name (i.default_source_name, source_info_cb); - } - - private void update_source () - { - context.get_server_info (update_source_get_server_info_cb); - } - - private DBusMessage pulse_dbus_filter (DBusConnection connection, owned DBusMessage message, bool incoming) - { - if (message.get_message_type () == DBusMessageType.SIGNAL) { - string active_role_objp = _objp_role_alert; - if (_active_sink_input != -1) - active_role_objp = _sink_input_hash.get (_active_sink_input); - - if (message.get_path () == active_role_objp && message.get_member () == "VolumeUpdated") { - uint sig_count = 0; - lock (_pa_volume_sig_count) { - sig_count = _pa_volume_sig_count; - if (_pa_volume_sig_count > 0) - _pa_volume_sig_count--; - } - - /* We only care about signals if our internal count is zero */ - if (sig_count == 0) { - /* Extract volume and make sure it's not a side effect of us setting it */ - Variant body = message.get_body (); - Variant varray = body.get_child_value (0); - - uint32 type = 0, lvolume = 0; - VariantIter iter = varray.iterator (); - iter.next ("(uu)", &type, &lvolume); - /* Here we need to compare integer values to avoid rounding issues, so just - * using the volume values used by pulseaudio */ - PulseAudio.Volume cvolume = double_to_volume (_volume.volume); - if (lvolume != cvolume) { - /* Someone else changed the volume for this role, reflect on the indicator */ - var vol = new VolumeControl.Volume(); - vol.volume = volume_to_double (lvolume); - vol.reason = VolumeControl.VolumeReasons.PULSE_CHANGE; - this.volume = vol; - } - } - } - } - - return message; - } - - private VolumeControl.Stream calculate_active_stream() - { - if (_active_sink_input != -1) { - var path = _sink_input_hash[_active_sink_input]; - if (path == _objp_role_multimedia) - return Stream.MULTIMEDIA; - if (path == _objp_role_alarm) - return Stream.ALARM; - if (path == _objp_role_phone) - return Stream.PHONE; - } - - return VolumeControl.Stream.ALERT; - } - - private async void update_active_sink_input (int32 index) - { - if ((index == -1) || (index != _active_sink_input && index in _sink_input_list)) { - string sink_input_objp = _objp_role_alert; - if (index != -1) - sink_input_objp = _sink_input_hash.get (index); - _active_sink_input = index; - var stream = calculate_active_stream(); - if (active_stream != stream) { - active_stream = stream; - } - - /* Listen for role volume changes from pulse itself (external clients) */ - try { - var builder = new VariantBuilder (VariantType.OBJECT_PATH_ARRAY); - builder.add ("o", sink_input_objp); - - yield _pconn.call ("org.PulseAudio.Core1", "/org/pulseaudio/core1", - "org.PulseAudio.Core1", "ListenForSignal", - new Variant ("(sao)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry.VolumeUpdated", builder), - null, DBusCallFlags.NONE, -1); - } catch (GLib.Error e) { - warning ("unable to listen for pulseaudio dbus signals (%s)", e.message); - } - - try { - var props_variant = yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry", - sink_input_objp, "org.freedesktop.DBus.Properties", "Get", - new Variant ("(ss)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume"), - null, DBusCallFlags.NONE, -1); - Variant tmp; - props_variant.get ("(v)", out tmp); - uint32 type = 0, volume = 0; - VariantIter iter = tmp.iterator (); - iter.next ("(uu)", &type, &volume); - - var vol = new VolumeControl.Volume(); - vol.volume = volume_to_double (volume); - vol.reason = VolumeControl.VolumeReasons.VOLUME_STREAM_CHANGE; - this.volume = vol; - } catch (GLib.Error e) { - warning ("unable to get volume for active role %s (%s)", sink_input_objp, e.message); - } - } - } - - private void add_sink_input_into_list (SinkInputInfo sink_input) - { - /* We're only adding ones that are not corked and with a valid role */ - unowned string role = sink_input.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE); - - if (role != null && role in _valid_roles) { - if (sink_input.corked == 0 || role == "phone") { - _sink_input_list.insert (0, sink_input.index); - switch (role) - { - case "multimedia": - _sink_input_hash.set (sink_input.index, _objp_role_multimedia); - break; - case "alert": - _sink_input_hash.set (sink_input.index, _objp_role_alert); - break; - case "alarm": - _sink_input_hash.set (sink_input.index, _objp_role_alarm); - break; - case "phone": - _sink_input_hash.set (sink_input.index, _objp_role_phone); - break; - } - /* Only switch the active sink input in case a phone one is not active */ - if (_active_sink_input == -1 || - _sink_input_hash.get (_active_sink_input) != _objp_role_phone) - update_active_sink_input.begin ((int32)sink_input.index); - } - } - } - - private void remove_sink_input_from_list (uint32 index) - { - if (index in _sink_input_list) { - _sink_input_list.remove (index); - _sink_input_hash.unset (index); - if (index == _active_sink_input) { - if (_sink_input_list.size != 0) - update_active_sink_input.begin ((int32)_sink_input_list.get (0)); - else - update_active_sink_input.begin (-1); - } - } - } - - private void handle_new_sink_input_cb (Context c, SinkInputInfo? i, int eol) - { - if (i == null) - return; - - add_sink_input_into_list (i); - } - - private void handle_changed_sink_input_cb (Context c, SinkInputInfo? i, int eol) - { - if (i == null) - return; - - if (i.index in _sink_input_list) { - /* Phone stream is always corked, so handle it differently */ - if (i.corked == 1 && _sink_input_hash.get (i.index) != _objp_role_phone) - remove_sink_input_from_list (i.index); - } else { - if (i.corked == 0) - add_sink_input_into_list (i); - } - } - - private void source_output_info_cb (Context c, SourceOutputInfo? i, int eol) - { - if (i == null) - return; - - unowned string role = i.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE); - if (role == "phone" || role == "production") { - this.active_mic = true; - this._source_sink_mic_activated = true; - } - } - - private void context_state_callback (Context c) - { - switch (c.get_state ()) { - case Context.State.READY: - if (_pulse_use_stream_restore) { - c.subscribe (PulseAudio.Context.SubscriptionMask.SINK | - PulseAudio.Context.SubscriptionMask.SINK_INPUT | - PulseAudio.Context.SubscriptionMask.SOURCE | - PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT); - } else { - c.subscribe (PulseAudio.Context.SubscriptionMask.SINK | - PulseAudio.Context.SubscriptionMask.SOURCE | - PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT); - } - c.set_subscribe_callback (context_events_cb); - update_sink (); - update_source (); - this.ready = true; // true because we're connected to the pulse server - break; - - case Context.State.FAILED: - case Context.State.TERMINATED: - if (_reconnect_timer == 0) - _reconnect_timer = Timeout.add_seconds (2, reconnect_timeout); - break; - - default: - this.ready = false; - break; - } - } - - bool reconnect_timeout () - { - _reconnect_timer = 0; - reconnect_to_pulse (); - return Source.REMOVE; - } - - void reconnect_to_pulse () - { - if (this.ready) { - this.context.disconnect (); - this.context = null; - this.ready = false; - } + private unowned PulseAudio.GLibMainLoop loop = null; + + private uint _reconnect_timer = 0; + + private PulseAudio.Context context; + private bool _mute = true; + private VolumeControl.Volume _volume = new VolumeControl.Volume(); + private double _mic_volume = 0.0; + + /* Used by the pulseaudio stream restore extension */ + private DBusConnection _pconn; + /* Need both the list and hash so we can retrieve the last known sink-input after + * releasing the current active one (restoring back to the previous known role) */ + private Gee.ArrayList<uint32> _sink_input_list = new Gee.ArrayList<uint32> (); + private HashMap<uint32, string> _sink_input_hash = new HashMap<uint32, string> (); + private bool _pulse_use_stream_restore = false; + private int32 _active_sink_input = -1; + private string[] _valid_roles = {"multimedia", "alert", "alarm", "phone"}; + private string? _objp_role_multimedia = null; + private string? _objp_role_alert = null; + private string? _objp_role_alarm = null; + private string? _objp_role_phone = null; + private uint _pa_volume_sig_count = 0; + + private uint _local_volume_timer = 0; + private uint _accountservice_volume_timer = 0; + private bool _send_next_local_volume = false; + private double _account_service_volume = 0.0; + private VolumeControl.ActiveOutput _active_output = VolumeControl.ActiveOutput.SPEAKERS; + private AccountsServiceAccess _accounts_service_access; + private bool _external_mic_detected = false; + private bool _source_sink_mic_activated = false; + + /** true when a microphone is active **/ + public override bool active_mic { get; set; default = false; } + + public VolumeControlPulse (IndicatorSound.Options options, PulseAudio.GLibMainLoop loop, AccountsServiceAccess? accounts_service_access) + { + base(options); + + _volume.volume = 0.0; + _volume.reason = VolumeControl.VolumeReasons.PULSE_CHANGE; + + this.loop = loop; + + _accounts_service_access = accounts_service_access; + this._accounts_service_access.notify["volume"].connect(() => { + if (this._accounts_service_access.volume >= 0 && _account_service_volume != this._accounts_service_access.volume) { + _account_service_volume = this._accounts_service_access.volume; + // we need to wait for this to settle. + start_account_service_volume_timer(); + } + }); + this.reconnect_to_pulse (); + } + + ~VolumeControlPulse () + { + stop_all_timers(); + } + + private void stop_all_timers() + { + if (_reconnect_timer != 0) { + Source.remove (_reconnect_timer); + _reconnect_timer = 0; + } + stop_local_volume_timer(); + stop_account_service_volume_timer(); + } + + public static VolumeControl.ActiveOutput calculate_active_output (SinkInfo? sink) { + + VolumeControl.ActiveOutput ret_output = VolumeControl.ActiveOutput.SPEAKERS; + /* Check if the current active port is headset/headphone */ + /* There is not easy way to check if the port is a headset/headphone besides + * checking for the port name. On touch (with the pulseaudio droid element) + * the headset/headphone port is called 'output-headset' and 'output-headphone'. + * On the desktop this is usually called 'analog-output-headphones' */ + + // first of all check if we are in call mode + if (sink.active_port != null && sink.active_port.name == "output-speaker+wired_headphone") { + return VolumeControl.ActiveOutput.CALL_MODE; + } + // look if it's a headset/headphones + if (sink.name == "indicator_sound_test_headphones" || + (sink.active_port != null && + (sink.active_port.name.contains("headset") || + sink.active_port.name.contains("headphone")))) { + // check if it's a bluetooth device + unowned string device_bus = sink.proplist.gets ("device.bus"); + if (device_bus != null && device_bus == "bluetooth") { + ret_output = VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES; + } else if (device_bus != null && device_bus == "usb") { + ret_output = VolumeControl.ActiveOutput.USB_HEADPHONES; + } else if (device_bus != null && device_bus == "hdmi") { + ret_output = VolumeControl.ActiveOutput.HDMI_HEADPHONES; + } else { + ret_output = VolumeControl.ActiveOutput.HEADPHONES; + } + } else { + // speaker + unowned string device_bus = sink.proplist.gets ("device.bus"); + if (device_bus != null && device_bus == "bluetooth") { + ret_output = VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER; + } else if (device_bus != null && device_bus == "usb") { + ret_output = VolumeControl.ActiveOutput.USB_SPEAKER; + } else if (device_bus != null && device_bus == "hdmi") { + ret_output = VolumeControl.ActiveOutput.HDMI_SPEAKER; + } else { + ret_output = VolumeControl.ActiveOutput.SPEAKERS; + } + } + + return ret_output; + } + + private bool is_external_mic (SourceInfo? sink) { + if (sink.name.contains ("indicator_sound_test_mic")) { + return true; + } + if (sink.active_port != null && + ( (sink.active_port.name.contains ("headphone") || + sink.active_port.name.contains ("headset") || + sink.active_port.name.contains ("mic") ) && + (!sink.active_port.name.contains ("internal") && + !sink.active_port.name.contains ("builtin")) )) { + return true; + } + return false; + } + + + /* PulseAudio logic*/ + private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index) + { + switch (t & Context.SubscriptionEventType.FACILITY_MASK) + { + case Context.SubscriptionEventType.SINK: + update_sink (); + break; + + case Context.SubscriptionEventType.SINK_INPUT: + switch (t & Context.SubscriptionEventType.TYPE_MASK) + { + case Context.SubscriptionEventType.NEW: + c.get_sink_input_info (index, handle_new_sink_input_cb); + break; + + case Context.SubscriptionEventType.CHANGE: + c.get_sink_input_info (index, handle_changed_sink_input_cb); + break; + + case Context.SubscriptionEventType.REMOVE: + remove_sink_input_from_list (index); + break; + default: + debug ("Sink input event not known."); + break; + } + break; + + case Context.SubscriptionEventType.SOURCE: + update_source (); + break; + + case Context.SubscriptionEventType.SOURCE_OUTPUT: + switch (t & Context.SubscriptionEventType.TYPE_MASK) + { + case Context.SubscriptionEventType.NEW: + c.get_source_output_info (index, source_output_info_cb); + break; + + case Context.SubscriptionEventType.REMOVE: + this._source_sink_mic_activated = false; + this.active_mic = _external_mic_detected; + break; + default: + break; + } + break; + + default: + break; + } + } + + private void sink_info_cb_for_props (Context c, SinkInfo? i, int eol) + { + if (i == null) + return; + + if (_mute != (bool)i.mute) + { + _mute = (bool)i.mute; + this.notify_property ("mute"); + } + + var playing = (i.state == PulseAudio.SinkState.RUNNING); + if (is_playing != playing) + is_playing = playing; + + var oldval = _active_output; + var newval = calculate_active_output(i); + + _active_output = newval; + + // Emit a change signal iff CALL_MODE wasn't involved. (FIXME: yuck.) + if ((oldval != VolumeControl.ActiveOutput.CALL_MODE) && + (newval != VolumeControl.ActiveOutput.CALL_MODE) && + (oldval != newval)) { + this.active_output_changed (newval); + } + + if (_pulse_use_stream_restore == false && + _volume.volume != volume_to_double (i.volume.max ())) + { + var vol = new VolumeControl.Volume(); + vol.volume = volume_to_double (i.volume.max ()); + vol.reason = VolumeControl.VolumeReasons.PULSE_CHANGE; + this.volume = vol; + } + } + + private void source_info_cb (Context c, SourceInfo? i, int eol) + { + if (i == null) + return; + + if (is_external_mic (i)) { + this.active_mic = true; + _external_mic_detected = true; + } else { + this.active_mic = _source_sink_mic_activated; + _external_mic_detected = false; + } + + if (_mic_volume != volume_to_double (i.volume.values[0])) + { + _mic_volume = volume_to_double (i.volume.values[0]); + this.notify_property ("mic-volume"); + } + } + + private void server_info_cb_for_props (Context c, ServerInfo? i) + { + if (i == null) + return; + context.get_sink_info_by_name (i.default_sink_name, sink_info_cb_for_props); + } + + private void update_sink () + { + context.get_server_info (server_info_cb_for_props); + } + + private void update_source_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) { + if (i != null) + context.get_source_info_by_name (i.default_source_name, source_info_cb); + } + + private void update_source () + { + context.get_server_info (update_source_get_server_info_cb); + } + + private DBusMessage pulse_dbus_filter (DBusConnection connection, owned DBusMessage message, bool incoming) + { + if (message.get_message_type () == DBusMessageType.SIGNAL) { + string active_role_objp = _objp_role_alert; + if (_active_sink_input != -1) + active_role_objp = _sink_input_hash.get (_active_sink_input); + + if (message.get_path () == active_role_objp && message.get_member () == "VolumeUpdated") { + uint sig_count = 0; + lock (_pa_volume_sig_count) { + sig_count = _pa_volume_sig_count; + if (_pa_volume_sig_count > 0) + _pa_volume_sig_count--; + } + + /* We only care about signals if our internal count is zero */ + if (sig_count == 0) { + /* Extract volume and make sure it's not a side effect of us setting it */ + Variant body = message.get_body (); + Variant varray = body.get_child_value (0); + + uint32 type = 0, lvolume = 0; + VariantIter iter = varray.iterator (); + iter.next ("(uu)", &type, &lvolume); + /* Here we need to compare integer values to avoid rounding issues, so just + * using the volume values used by pulseaudio */ + PulseAudio.Volume cvolume = double_to_volume (_volume.volume); + if (lvolume != cvolume) { + /* Someone else changed the volume for this role, reflect on the indicator */ + var vol = new VolumeControl.Volume(); + vol.volume = volume_to_double (lvolume); + vol.reason = VolumeControl.VolumeReasons.PULSE_CHANGE; + this.volume = vol; + } + } + } + } + + return message; + } + + private VolumeControl.Stream calculate_active_stream() + { + if (_active_sink_input != -1) { + var path = _sink_input_hash[_active_sink_input]; + if (path == _objp_role_multimedia) + return Stream.MULTIMEDIA; + if (path == _objp_role_alarm) + return Stream.ALARM; + if (path == _objp_role_phone) + return Stream.PHONE; + } + + return VolumeControl.Stream.ALERT; + } + + private async void update_active_sink_input (int32 index) + { + if ((index == -1) || (index != _active_sink_input && index in _sink_input_list)) { + string sink_input_objp = _objp_role_alert; + if (index != -1) + sink_input_objp = _sink_input_hash.get (index); + _active_sink_input = index; + var stream = calculate_active_stream(); + if (active_stream != stream) { + active_stream = stream; + } + + /* Listen for role volume changes from pulse itself (external clients) */ + try { + var builder = new VariantBuilder (VariantType.OBJECT_PATH_ARRAY); + builder.add ("o", sink_input_objp); + + yield _pconn.call ("org.PulseAudio.Core1", "/org/pulseaudio/core1", + "org.PulseAudio.Core1", "ListenForSignal", + new Variant ("(sao)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry.VolumeUpdated", builder), + null, DBusCallFlags.NONE, -1); + } catch (GLib.Error e) { + warning ("unable to listen for pulseaudio dbus signals (%s)", e.message); + } + + try { + var props_variant = yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry", + sink_input_objp, "org.freedesktop.DBus.Properties", "Get", + new Variant ("(ss)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume"), + null, DBusCallFlags.NONE, -1); + Variant tmp; + props_variant.get ("(v)", out tmp); + uint32 type = 0, volume = 0; + VariantIter iter = tmp.iterator (); + iter.next ("(uu)", &type, &volume); + + var vol = new VolumeControl.Volume(); + vol.volume = volume_to_double (volume); + vol.reason = VolumeControl.VolumeReasons.VOLUME_STREAM_CHANGE; + this.volume = vol; + } catch (GLib.Error e) { + warning ("unable to get volume for active role %s (%s)", sink_input_objp, e.message); + } + } + } + + private void add_sink_input_into_list (SinkInputInfo sink_input) + { + /* We're only adding ones that are not corked and with a valid role */ + unowned string role = sink_input.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE); + + if (role != null && role in _valid_roles) { + if (sink_input.corked == 0 || role == "phone") { + _sink_input_list.insert (0, sink_input.index); + switch (role) + { + case "multimedia": + _sink_input_hash.set (sink_input.index, _objp_role_multimedia); + break; + case "alert": + _sink_input_hash.set (sink_input.index, _objp_role_alert); + break; + case "alarm": + _sink_input_hash.set (sink_input.index, _objp_role_alarm); + break; + case "phone": + _sink_input_hash.set (sink_input.index, _objp_role_phone); + break; + } + /* Only switch the active sink input in case a phone one is not active */ + if (_active_sink_input == -1 || + _sink_input_hash.get (_active_sink_input) != _objp_role_phone) + update_active_sink_input.begin ((int32)sink_input.index); + } + } + } + + private void remove_sink_input_from_list (uint32 index) + { + if (index in _sink_input_list) { + _sink_input_list.remove (index); + _sink_input_hash.unset (index); + if (index == _active_sink_input) { + if (_sink_input_list.size != 0) + update_active_sink_input.begin ((int32)_sink_input_list.get (0)); + else + update_active_sink_input.begin (-1); + } + } + } + + private void handle_new_sink_input_cb (Context c, SinkInputInfo? i, int eol) + { + if (i == null) + return; + + add_sink_input_into_list (i); + } + + private void handle_changed_sink_input_cb (Context c, SinkInputInfo? i, int eol) + { + if (i == null) + return; + + if (i.index in _sink_input_list) { + /* Phone stream is always corked, so handle it differently */ + if (i.corked == 1 && _sink_input_hash.get (i.index) != _objp_role_phone) + remove_sink_input_from_list (i.index); + } else { + if (i.corked == 0) + add_sink_input_into_list (i); + } + } + + private void source_output_info_cb (Context c, SourceOutputInfo? i, int eol) + { + if (i == null) + return; + + unowned string role = i.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE); + if (role == "phone" || role == "production") { + this.active_mic = true; + this._source_sink_mic_activated = true; + } + } + + private void context_state_callback (Context c) + { + switch (c.get_state ()) { + case Context.State.READY: + if (_pulse_use_stream_restore) { + c.subscribe (PulseAudio.Context.SubscriptionMask.SINK | + PulseAudio.Context.SubscriptionMask.SINK_INPUT | + PulseAudio.Context.SubscriptionMask.SOURCE | + PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT); + } else { + c.subscribe (PulseAudio.Context.SubscriptionMask.SINK | + PulseAudio.Context.SubscriptionMask.SOURCE | + PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT); + } + c.set_subscribe_callback (context_events_cb); + update_sink (); + update_source (); + this.ready = true; // true because we're connected to the pulse server + break; + + case Context.State.FAILED: + case Context.State.TERMINATED: + if (_reconnect_timer == 0) + _reconnect_timer = Timeout.add_seconds (2, reconnect_timeout); + break; + + default: + this.ready = false; + break; + } + } + + bool reconnect_timeout () + { + _reconnect_timer = 0; + reconnect_to_pulse (); + return Source.REMOVE; + } + + void reconnect_to_pulse () + { + if (this.ready) { + this.context.disconnect (); + this.context = null; + this.ready = false; + } /* FIXME: Ubuntu Settings Daemon specifics */ - var props = new Proplist (); - props.sets (Proplist.PROP_APPLICATION_NAME, "Ubuntu Audio Settings"); - props.sets (Proplist.PROP_APPLICATION_ID, "com.canonical.settings.sound"); - props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control"); - props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1"); - - reconnect_pulse_dbus (); - - this.context = new PulseAudio.Context (loop.get_api(), null, props); - this.context.set_state_callback (context_state_callback); - - unowned string server_string = Environment.get_variable("PULSE_SERVER"); - if (context.connect(server_string, Context.Flags.NOFAIL, null) < 0) - warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno())); - } - - void sink_info_list_callback_set_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) { - if (sink != null) - context.set_sink_mute_by_index (sink.index, true, null); - } - - void sink_info_list_callback_unset_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) { - if (sink != null) - context.set_sink_mute_by_index (sink.index, false, null); - } - - /* Mute operations */ - bool set_mute_internal (bool mute) - { - return_val_if_fail (context.get_state () == Context.State.READY, false); - - if (_mute != mute) { - if (mute) - context.get_sink_info_list (sink_info_list_callback_set_mute); - else - context.get_sink_info_list (sink_info_list_callback_unset_mute); - return true; - } else { - return false; - } - } - - public override void set_mute (bool mute) - { - if (set_mute_internal (mute)) - _accounts_service_access.mute = mute; - } - - public void toggle_mute () - { - this.set_mute (!this._mute); - } - - public override bool mute - { - get - { - return this._mute; - } - } - - public override VolumeControl.ActiveOutput active_output() - { - return _active_output; - } - - /* Volume operations */ - public static PulseAudio.Volume double_to_volume (double vol) - { - double tmp = (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED) * vol; - return (PulseAudio.Volume)tmp + PulseAudio.Volume.MUTED; - } - - public static double volume_to_double (PulseAudio.Volume vol) - { - double tmp = (double)(vol - PulseAudio.Volume.MUTED); - return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED); - } - - private void set_volume_success_cb (Context c, int success) - { - if ((bool)success) - this.notify_property("volume"); - } - - private void sink_info_set_volume_cb (Context c, SinkInfo? i, int eol) - { - if (i == null) - return; - - unowned CVolume cvol = i.volume; - cvol.scale (double_to_volume (_volume.volume)); - c.set_sink_volume_by_index (i.index, cvol, set_volume_success_cb); - } - - private void server_info_cb_for_set_volume (Context c, ServerInfo? i) - { - if (i == null) - { - warning ("Could not get PulseAudio server info"); - return; - } - - context.get_sink_info_by_name (i.default_sink_name, sink_info_set_volume_cb); - } - - private async void set_volume_active_role () - { - string active_role_objp = _objp_role_alert; - - if (_active_sink_input != -1 && _active_sink_input in _sink_input_list) - active_role_objp = _sink_input_hash.get (_active_sink_input); - - try { - double vol = _volume.volume; - var builder = new VariantBuilder (new VariantType ("a(uu)")); - builder.add ("(uu)", 0, double_to_volume (vol)); - Variant volume = builder.end (); - - /* Increase the signal counter so we can handle the callback */ - lock (_pa_volume_sig_count) { - _pa_volume_sig_count++; - } - - yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry", - active_role_objp, "org.freedesktop.DBus.Properties", "Set", - new Variant ("(ssv)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume", volume), - null, DBusCallFlags.NONE, -1); - } catch (GLib.Error e) { - lock (_pa_volume_sig_count) { - _pa_volume_sig_count--; - } - warning ("unable to set volume for stream obj path %s (%s)", active_role_objp, e.message); - } - } - - void set_mic_volume_success_cb (Context c, int success) - { - if ((bool)success) - this.notify_property ("mic-volume"); - } - - void set_mic_volume_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) { - if (i != null) { - unowned CVolume cvol = CVolume (); - cvol.set (1, double_to_volume (_mic_volume)); - c.set_source_volume_by_name (i.default_source_name, cvol, set_mic_volume_success_cb); - } - } - - public override VolumeControl.Volume volume { - get { - return _volume; - } - set { - var volume_changed = (value.volume != _volume.volume); - debug("Setting volume to %f for profile %d because %d", value.volume, _active_sink_input, value.reason); - - _volume = value; - - /* Make sure we're connected to Pulse and pulse didn't give us the change */ - if (context.get_state () == Context.State.READY && - _volume.reason != VolumeControl.VolumeReasons.PULSE_CHANGE && - volume_changed) - if (_pulse_use_stream_restore) - set_volume_active_role.begin (); - else - context.get_server_info (server_info_cb_for_set_volume); - - - if (volume.reason != VolumeControl.VolumeReasons.ACCOUNTS_SERVICE_SET - && volume_changed) { - start_local_volume_timer(); - } - } - } - - /** MIC VOLUME PROPERTY */ - - public override double mic_volume { - get { - return _mic_volume; - } - set { - return_if_fail (context.get_state () == Context.State.READY); - - _mic_volume = value; - - context.get_server_info (set_mic_volume_get_server_info_cb); - } - } - - public static DBusConnection? create_pulse_dbus_connection() - { - unowned string pulse_dbus_server_env = Environment.get_variable ("PULSE_DBUS_SERVER"); - string address; - - if (pulse_dbus_server_env != null) { - address = pulse_dbus_server_env; - } else { - DBusConnection conn; - Variant props; - - try { - conn = Bus.get_sync (BusType.SESSION); - } catch (GLib.IOError e) { - warning ("unable to get the dbus session bus: %s", e.message); - return null; - } - - try { - var props_variant = conn.call_sync ("org.PulseAudio1", - "/org/pulseaudio/server_lookup1", "org.freedesktop.DBus.Properties", - "Get", new Variant ("(ss)", "org.PulseAudio.ServerLookup1", "Address"), - null, DBusCallFlags.NONE, -1); - props_variant.get ("(v)", out props); - address = props.get_string (); - } catch (GLib.Error e) { - warning ("unable to get pulse unix socket: %s", e.message); - return null; - } - } - - DBusConnection conn = null; - try { - conn = new DBusConnection.for_address_sync (address, DBusConnectionFlags.AUTHENTICATION_CLIENT); - } catch (GLib.Error e) { - GLib.warning("Unable to connect to dbus server at '%s': %s", address, e.message); - /* If it fails, it means the dbus pulse extension is not available */ - } - GLib.debug ("PulseAudio dbus address is '%s', connection is '%p'", address, conn); - return conn; - } - - /* PulseAudio Dbus (Stream Restore) logic */ - private void reconnect_pulse_dbus () - { - /* In case of a reconnect */ - _pulse_use_stream_restore = false; - _pa_volume_sig_count = 0; - - _pconn = create_pulse_dbus_connection(); - if (_pconn == null) - return; - - /* For pulse dbus related events */ - _pconn.add_filter (pulse_dbus_filter); - - /* Check if the 4 currently supported media roles are already available in StreamRestore - * Roles: multimedia, alert, alarm and phone */ - _objp_role_multimedia = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:multimedia"); - _objp_role_alert = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:alert"); - _objp_role_alarm = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:alarm"); - _objp_role_phone = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:phone"); - - /* Only use stream restore if every used role is available */ - if (_objp_role_multimedia != null && _objp_role_alert != null && _objp_role_alarm != null && _objp_role_phone != null) { - debug ("Using PulseAudio DBUS Stream Restore module"); - /* Restore volume and update default entry */ - update_active_sink_input.begin (-1); - _pulse_use_stream_restore = true; - } - } - - public static string? stream_restore_get_object_path (DBusConnection pconn, string name) { - string? objp = null; - try { - Variant props_variant = pconn.call_sync ("org.PulseAudio.Ext.StreamRestore1", - "/org/pulseaudio/stream_restore1", "org.PulseAudio.Ext.StreamRestore1", - "GetEntryByName", new Variant ("(s)", name), null, DBusCallFlags.NONE, -1); - /* Workaround for older versions of vala that don't provide get_objv */ - VariantIter iter = props_variant.iterator (); - iter.next ("o", &objp); - debug ("Found obj path %s for restore data named %s\n", objp, name); - } catch (GLib.Error e) { - warning ("unable to find stream restore data for: %s. Error: %s", name, e.message); - } - return objp; - } - - /* AccountsService operations */ - - private void start_local_volume_timer() - { - // perform a slow sync with the accounts service. max at 1 per second. - - // stop the AS update timer, as since we're going to be setting the volume. - stop_account_service_volume_timer(); - - if (_local_volume_timer == 0) { - _accounts_service_access.volume = _volume.volume; - _local_volume_timer = Timeout.add_seconds (1, local_volume_changed_timeout); - } else { - _send_next_local_volume = true; - } - } - - private void stop_local_volume_timer() - { - if (_local_volume_timer != 0) { - Source.remove (_local_volume_timer); - _local_volume_timer = 0; - } - } - - bool local_volume_changed_timeout() - { - _local_volume_timer = 0; - if (_send_next_local_volume) { - _send_next_local_volume = false; - start_local_volume_timer (); - } - return Source.REMOVE; - } - - private void start_account_service_volume_timer() - { - if (_accountservice_volume_timer == 0) { - // If we haven't been messing with local volume recently, apply immediately. - if (_local_volume_timer == 0) { - var vol = new VolumeControl.Volume(); - vol.volume = _account_service_volume; - vol.reason = VolumeControl.VolumeReasons.ACCOUNTS_SERVICE_SET; - this.volume = vol; - return; - } - // Else check again in another second if needed. - // (if AS is throwing us lots of notifications, we update at most once a second) - _accountservice_volume_timer = Timeout.add_seconds (1, accountservice_volume_changed_timeout); - } - } - - private void stop_account_service_volume_timer() - { - if (_accountservice_volume_timer != 0) { - Source.remove (_accountservice_volume_timer); - _accountservice_volume_timer = 0; - } - } - - bool accountservice_volume_changed_timeout () - { - _accountservice_volume_timer = 0; - start_account_service_volume_timer (); - return Source.REMOVE; - } + var props = new Proplist (); + props.sets (Proplist.PROP_APPLICATION_NAME, "Ubuntu Audio Settings"); + props.sets (Proplist.PROP_APPLICATION_ID, "com.canonical.settings.sound"); + props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control"); + props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1"); + + reconnect_pulse_dbus (); + + this.context = new PulseAudio.Context (loop.get_api(), null, props); + this.context.set_state_callback (context_state_callback); + + unowned string server_string = Environment.get_variable("PULSE_SERVER"); + if (context.connect(server_string, Context.Flags.NOFAIL, null) < 0) + warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno())); + } + + void sink_info_list_callback_set_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) { + if (sink != null) + context.set_sink_mute_by_index (sink.index, true, null); + } + + void sink_info_list_callback_unset_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) { + if (sink != null) + context.set_sink_mute_by_index (sink.index, false, null); + } + + /* Mute operations */ + bool set_mute_internal (bool mute) + { + return_val_if_fail (context.get_state () == Context.State.READY, false); + + if (_mute != mute) { + if (mute) + context.get_sink_info_list (sink_info_list_callback_set_mute); + else + context.get_sink_info_list (sink_info_list_callback_unset_mute); + return true; + } else { + return false; + } + } + + public override void set_mute (bool mute) + { + if (set_mute_internal (mute)) + _accounts_service_access.mute = mute; + } + + public void toggle_mute () + { + this.set_mute (!this._mute); + } + + public override bool mute + { + get + { + return this._mute; + } + } + + public override VolumeControl.ActiveOutput active_output() + { + return _active_output; + } + + /* Volume operations */ + public static PulseAudio.Volume double_to_volume (double vol) + { + double tmp = (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED) * vol; + return (PulseAudio.Volume)tmp + PulseAudio.Volume.MUTED; + } + + public static double volume_to_double (PulseAudio.Volume vol) + { + double tmp = (double)(vol - PulseAudio.Volume.MUTED); + return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED); + } + + private void set_volume_success_cb (Context c, int success) + { + if ((bool)success) + this.notify_property("volume"); + } + + private void sink_info_set_volume_cb (Context c, SinkInfo? i, int eol) + { + if (i == null) + return; + + unowned CVolume cvol = i.volume; + cvol.scale (double_to_volume (_volume.volume)); + c.set_sink_volume_by_index (i.index, cvol, set_volume_success_cb); + } + + private void server_info_cb_for_set_volume (Context c, ServerInfo? i) + { + if (i == null) + { + warning ("Could not get PulseAudio server info"); + return; + } + + context.get_sink_info_by_name (i.default_sink_name, sink_info_set_volume_cb); + } + + private async void set_volume_active_role () + { + string active_role_objp = _objp_role_alert; + + if (_active_sink_input != -1 && _active_sink_input in _sink_input_list) + active_role_objp = _sink_input_hash.get (_active_sink_input); + + try { + double vol = _volume.volume; + var builder = new VariantBuilder (new VariantType ("a(uu)")); + builder.add ("(uu)", 0, double_to_volume (vol)); + Variant volume = builder.end (); + + /* Increase the signal counter so we can handle the callback */ + lock (_pa_volume_sig_count) { + _pa_volume_sig_count++; + } + + yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry", + active_role_objp, "org.freedesktop.DBus.Properties", "Set", + new Variant ("(ssv)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume", volume), + null, DBusCallFlags.NONE, -1); + } catch (GLib.Error e) { + lock (_pa_volume_sig_count) { + _pa_volume_sig_count--; + } + warning ("unable to set volume for stream obj path %s (%s)", active_role_objp, e.message); + } + } + + void set_mic_volume_success_cb (Context c, int success) + { + if ((bool)success) + this.notify_property ("mic-volume"); + } + + void set_mic_volume_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) { + if (i != null) { + unowned CVolume cvol = CVolume (); + cvol.set (1, double_to_volume (_mic_volume)); + c.set_source_volume_by_name (i.default_source_name, cvol, set_mic_volume_success_cb); + } + } + + public override VolumeControl.Volume volume { + get { + return _volume; + } + set { + var volume_changed = (value.volume != _volume.volume); + debug("Setting volume to %f for profile %d because %d", value.volume, _active_sink_input, value.reason); + + _volume = value; + + /* Make sure we're connected to Pulse and pulse didn't give us the change */ + if (context.get_state () == Context.State.READY && + _volume.reason != VolumeControl.VolumeReasons.PULSE_CHANGE && + volume_changed) + if (_pulse_use_stream_restore) + set_volume_active_role.begin (); + else + context.get_server_info (server_info_cb_for_set_volume); + + + if (volume.reason != VolumeControl.VolumeReasons.ACCOUNTS_SERVICE_SET + && volume_changed) { + start_local_volume_timer(); + } + } + } + + /** MIC VOLUME PROPERTY */ + + public override double mic_volume { + get { + return _mic_volume; + } + set { + return_if_fail (context.get_state () == Context.State.READY); + + _mic_volume = value; + + context.get_server_info (set_mic_volume_get_server_info_cb); + } + } + + public static DBusConnection? create_pulse_dbus_connection() + { + unowned string pulse_dbus_server_env = Environment.get_variable ("PULSE_DBUS_SERVER"); + string address; + + if (pulse_dbus_server_env != null) { + address = pulse_dbus_server_env; + } else { + DBusConnection conn; + Variant props; + + try { + conn = Bus.get_sync (BusType.SESSION); + } catch (GLib.IOError e) { + warning ("unable to get the dbus session bus: %s", e.message); + return null; + } + + try { + var props_variant = conn.call_sync ("org.PulseAudio1", + "/org/pulseaudio/server_lookup1", "org.freedesktop.DBus.Properties", + "Get", new Variant ("(ss)", "org.PulseAudio.ServerLookup1", "Address"), + null, DBusCallFlags.NONE, -1); + props_variant.get ("(v)", out props); + address = props.get_string (); + } catch (GLib.Error e) { + warning ("unable to get pulse unix socket: %s", e.message); + return null; + } + } + + DBusConnection conn = null; + try { + conn = new DBusConnection.for_address_sync (address, DBusConnectionFlags.AUTHENTICATION_CLIENT); + } catch (GLib.Error e) { + GLib.warning("Unable to connect to dbus server at '%s': %s", address, e.message); + /* If it fails, it means the dbus pulse extension is not available */ + } + GLib.debug ("PulseAudio dbus address is '%s', connection is '%p'", address, conn); + return conn; + } + + /* PulseAudio Dbus (Stream Restore) logic */ + private void reconnect_pulse_dbus () + { + /* In case of a reconnect */ + _pulse_use_stream_restore = false; + _pa_volume_sig_count = 0; + + _pconn = create_pulse_dbus_connection(); + if (_pconn == null) + return; + + /* For pulse dbus related events */ + _pconn.add_filter (pulse_dbus_filter); + + /* Check if the 4 currently supported media roles are already available in StreamRestore + * Roles: multimedia, alert, alarm and phone */ + _objp_role_multimedia = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:multimedia"); + _objp_role_alert = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:alert"); + _objp_role_alarm = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:alarm"); + _objp_role_phone = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:phone"); + + /* Only use stream restore if every used role is available */ + if (_objp_role_multimedia != null && _objp_role_alert != null && _objp_role_alarm != null && _objp_role_phone != null) { + debug ("Using PulseAudio DBUS Stream Restore module"); + /* Restore volume and update default entry */ + update_active_sink_input.begin (-1); + _pulse_use_stream_restore = true; + } + } + + public static string? stream_restore_get_object_path (DBusConnection pconn, string name) { + string? objp = null; + try { + Variant props_variant = pconn.call_sync ("org.PulseAudio.Ext.StreamRestore1", + "/org/pulseaudio/stream_restore1", "org.PulseAudio.Ext.StreamRestore1", + "GetEntryByName", new Variant ("(s)", name), null, DBusCallFlags.NONE, -1); + /* Workaround for older versions of vala that don't provide get_objv */ + VariantIter iter = props_variant.iterator (); + iter.next ("o", &objp); + debug ("Found obj path %s for restore data named %s\n", objp, name); + } catch (GLib.Error e) { + warning ("unable to find stream restore data for: %s. Error: %s", name, e.message); + } + return objp; + } + + /* AccountsService operations */ + + private void start_local_volume_timer() + { + // perform a slow sync with the accounts service. max at 1 per second. + + // stop the AS update timer, as since we're going to be setting the volume. + stop_account_service_volume_timer(); + + if (_local_volume_timer == 0) { + _accounts_service_access.volume = _volume.volume; + _local_volume_timer = Timeout.add_seconds (1, local_volume_changed_timeout); + } else { + _send_next_local_volume = true; + } + } + + private void stop_local_volume_timer() + { + if (_local_volume_timer != 0) { + Source.remove (_local_volume_timer); + _local_volume_timer = 0; + } + } + + bool local_volume_changed_timeout() + { + _local_volume_timer = 0; + if (_send_next_local_volume) { + _send_next_local_volume = false; + start_local_volume_timer (); + } + return Source.REMOVE; + } + + private void start_account_service_volume_timer() + { + if (_accountservice_volume_timer == 0) { + // If we haven't been messing with local volume recently, apply immediately. + if (_local_volume_timer == 0) { + var vol = new VolumeControl.Volume(); + vol.volume = _account_service_volume; + vol.reason = VolumeControl.VolumeReasons.ACCOUNTS_SERVICE_SET; + this.volume = vol; + return; + } + // Else check again in another second if needed. + // (if AS is throwing us lots of notifications, we update at most once a second) + _accountservice_volume_timer = Timeout.add_seconds (1, accountservice_volume_changed_timeout); + } + } + + private void stop_account_service_volume_timer() + { + if (_accountservice_volume_timer != 0) { + Source.remove (_accountservice_volume_timer); + _accountservice_volume_timer = 0; + } + } + + bool accountservice_volume_changed_timeout () + { + _accountservice_volume_timer = 0; + start_account_service_volume_timer (); + return Source.REMOVE; + } } diff --git a/src/volume-warning.vala b/src/volume-warning.vala index 659240b..cad74b6 100644 --- a/src/volume-warning.vala +++ b/src/volume-warning.vala @@ -2,6 +2,7 @@ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*- * * Copyright 2015 Canonical Ltd. + * Copyright 2021 Robert Tari * * 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 @@ -17,214 +18,215 @@ * * Authors: * Charles Kerr <charles.kerr@canonical.com> + * Robert Tari <robert@tari.in> */ using PulseAudio; public abstract class VolumeWarning : Object { - // true if headphones are in use - public bool headphones_active { get; set; default = false; } - - // true if the warning dialog is being shown - public bool active { get; protected set; default = false; } - - // true if we're playing unapproved loud multimedia over headphones - public bool high_volume { get; protected set; default = false; } - - public enum Key { - VOLUME_UP, - VOLUME_DOWN - } - - public void user_keypress (Key key) { - if ((key == Key.VOLUME_DOWN) && active) { - _notification.close (); - on_user_response (IndicatorSound.WarnNotification.Response.CANCEL); - } - } - - internal VolumeWarning (IndicatorSound.Options options) { - - _options = options; - - init_high_volume (); - init_approved (); - - _notification.user_responded.connect ((n, r) => on_user_response (r)); - } - - ~VolumeWarning () { - clear_timer (ref _approved_timer); - } - - /*** - **** - ***/ - - // true if the user has approved high volumes recently - protected bool approved { get; set; default = false; } - - // true if multimedia is currently playing - protected bool multimedia_active { get; set; default = false; } - - /* Cached value of the multimedia volume reported by pulse. - Setting this only updates the cache -- to change the volume, - use sound_system_set_multimedia_volume. - NB: This PulseAudio.Volume is typed as uint to unconfuse valac. */ - protected uint multimedia_volume { get; set; default = PulseAudio.Volume.INVALID; } - - protected abstract void sound_system_set_multimedia_volume (PulseAudio.Volume volume); - - protected void clear_timer (ref uint timer) { - if (timer != 0) { - Source.remove (timer); - timer = 0; - } - } - - private IndicatorSound.Options _options; - - /** - *** HIGH VOLUME PROPERTY - **/ - - private void init_high_volume () { - const string self_keys[] = { - "multimedia-volume", - "multimedia-active", - "headphones-active", - "high-volume-approved" - }; - foreach (var key in self_keys) - this.notify[key].connect (() => update_high_volume ()); - - const string options_keys[] = { - "loud-volume", - "loud-warning-enabled" - }; - foreach (var key in options_keys) - _options.notify[key].connect (() => update_high_volume ()); - - update_high_volume (); - } - - private void update_high_volume () { - - var newval = _options.loud_warning_enabled - && headphones_active - && multimedia_active - && !approved - && (multimedia_volume != PulseAudio.Volume.INVALID) - // from the sound specs: - // "Whenever you increase volume,..., such that acoustic output would be MORE than 85 dB - && (multimedia_volume > _options.loud_volume); - - if (high_volume != newval) { - debug ("changing high_volume from %d to %d", (int)high_volume, (int)newval); - if (newval && !active) - activate (); - high_volume = newval; - } - } - - /** - *** HIGH VOLUME APPROVED PROPERTY - **/ - - private Settings _settings = new Settings ("org.ayatana.indicator.sound"); - private static const string TTL_KEY = "warning-volume-confirmation-ttl"; - private uint _approved_timer = 0; - private int64 _approved_at = 0; - private int64 _approved_ttl_usec = 0; - - private void approve_high_volume () { - _approved_at = GLib.get_monotonic_time (); - update_approved (); - update_approved_timer (); - } - - private void init_approved () { - _settings.changed[TTL_KEY].connect (() => update_approved_cache ()); - update_approved_cache (); - } - private void update_approved_cache () { - _approved_ttl_usec = _settings.get_int (TTL_KEY); - _approved_ttl_usec *= 1000000; - - update_approved (); - update_approved_timer (); - } - private void update_approved_timer () { - - clear_timer (ref _approved_timer); - - if (_approved_at == 0) - return; - - int64 expires_at = _approved_at + _approved_ttl_usec; - int64 now = GLib.get_monotonic_time (); - if (expires_at > now) { - var seconds_left = 1 + ((expires_at - now) / 1000000); - _approved_timer = Timeout.add_seconds ((uint)seconds_left, () => { - _approved_timer = 0; - update_approved (); - return Source.REMOVE; - }); - } - } - private void update_approved () { - var new_approved = calculate_approved (); - if (approved != new_approved) { - debug ("changing approved from %d to %d", (int)approved, (int)new_approved); - approved = new_approved; - } - } - private bool calculate_approved () { - int64 now = GLib.get_monotonic_time (); - return (_approved_at != 0) - && (_approved_at + _approved_ttl_usec >= now); - } - - // NOTIFICATION - - private IndicatorSound.WarnNotification _notification = new IndicatorSound.WarnNotification (); - private PulseAudio.Volume _ok_volume = PulseAudio.Volume.INVALID; - - protected virtual void preshow () {} - - private void activate () { - preshow (); - _ok_volume = multimedia_volume; - - _notification.show (); - this.active = true; - - // lower the volume to just the warning level - // from the sound specs: - // "Whenever you increase volume,..., such that acoustic output would be MORE than 85 dB - sound_system_set_multimedia_volume (_options.loud_volume); - } - - private void on_user_response (IndicatorSound.WarnNotification.Response response) { - - this.active = false; - - if (response == IndicatorSound.WarnNotification.Response.OK) { - approve_high_volume (); - sound_system_set_multimedia_volume (_ok_volume); - } else { - this.cancel_pressed (this.volume_to_double(_options.loud_volume)); - } - - _ok_volume = PulseAudio.Volume.INVALID; - } - - private static double volume_to_double (PulseAudio.Volume vol) - { - double tmp = (double)(vol - PulseAudio.Volume.MUTED); - return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED); - } - - public signal void cancel_pressed (double cancel_volume); + // true if headphones are in use + public bool headphones_active { get; set; default = false; } + + // true if the warning dialog is being shown + public bool active { get; protected set; default = false; } + + // true if we're playing unapproved loud multimedia over headphones + public bool high_volume { get; protected set; default = false; } + + public enum Key { + VOLUME_UP, + VOLUME_DOWN + } + + public void user_keypress (Key key) { + if ((key == Key.VOLUME_DOWN) && active) { + _notification.close (); + on_user_response (IndicatorSound.WarnNotification.Response.CANCEL); + } + } + + internal VolumeWarning (IndicatorSound.Options options) { + + _options = options; + + init_high_volume (); + init_approved (); + + _notification.user_responded.connect ((n, r) => on_user_response (r)); + } + + ~VolumeWarning () { + clear_timer (ref _approved_timer); + } + + /*** + **** + ***/ + + // true if the user has approved high volumes recently + protected bool approved { get; set; default = false; } + + // true if multimedia is currently playing + protected bool multimedia_active { get; set; default = false; } + + /* Cached value of the multimedia volume reported by pulse. + Setting this only updates the cache -- to change the volume, + use sound_system_set_multimedia_volume. + NB: This PulseAudio.Volume is typed as uint to unconfuse valac. */ + protected uint multimedia_volume { get; set; default = PulseAudio.Volume.INVALID; } + + protected abstract void sound_system_set_multimedia_volume (PulseAudio.Volume volume); + + protected void clear_timer (ref uint timer) { + if (timer != 0) { + Source.remove (timer); + timer = 0; + } + } + + private IndicatorSound.Options _options; + + /** + *** HIGH VOLUME PROPERTY + **/ + + private void init_high_volume () { + const string self_keys[] = { + "multimedia-volume", + "multimedia-active", + "headphones-active", + "high-volume-approved" + }; + foreach (var key in self_keys) + this.notify[key].connect (() => update_high_volume ()); + + const string options_keys[] = { + "loud-volume", + "loud-warning-enabled" + }; + foreach (var key in options_keys) + _options.notify[key].connect (() => update_high_volume ()); + + update_high_volume (); + } + + private void update_high_volume () { + + var newval = _options.loud_warning_enabled + && headphones_active + && multimedia_active + && !approved + && (multimedia_volume != PulseAudio.Volume.INVALID) + // from the sound specs: + // "Whenever you increase volume,..., such that acoustic output would be MORE than 85 dB + && (multimedia_volume > _options.loud_volume); + + if (high_volume != newval) { + debug ("changing high_volume from %d to %d", (int)high_volume, (int)newval); + if (newval && !active) + activate (); + high_volume = newval; + } + } + + /** + *** HIGH VOLUME APPROVED PROPERTY + **/ + + private Settings _settings = new Settings ("org.ayatana.indicator.sound"); + private const string TTL_KEY = "warning-volume-confirmation-ttl"; + private uint _approved_timer = 0; + private int64 _approved_at = 0; + private int64 _approved_ttl_usec = 0; + + private void approve_high_volume () { + _approved_at = GLib.get_monotonic_time (); + update_approved (); + update_approved_timer (); + } + + private void init_approved () { + _settings.changed[TTL_KEY].connect (() => update_approved_cache ()); + update_approved_cache (); + } + private void update_approved_cache () { + _approved_ttl_usec = _settings.get_int (TTL_KEY); + _approved_ttl_usec *= 1000000; + + update_approved (); + update_approved_timer (); + } + private void update_approved_timer () { + + clear_timer (ref _approved_timer); + + if (_approved_at == 0) + return; + + int64 expires_at = _approved_at + _approved_ttl_usec; + int64 now = GLib.get_monotonic_time (); + if (expires_at > now) { + var seconds_left = 1 + ((expires_at - now) / 1000000); + _approved_timer = Timeout.add_seconds ((uint)seconds_left, () => { + _approved_timer = 0; + update_approved (); + return Source.REMOVE; + }); + } + } + private void update_approved () { + var new_approved = calculate_approved (); + if (approved != new_approved) { + debug ("changing approved from %d to %d", (int)approved, (int)new_approved); + approved = new_approved; + } + } + private bool calculate_approved () { + int64 now = GLib.get_monotonic_time (); + return (_approved_at != 0) + && (_approved_at + _approved_ttl_usec >= now); + } + + // NOTIFICATION + + private IndicatorSound.WarnNotification _notification = new IndicatorSound.WarnNotification (); + private PulseAudio.Volume _ok_volume = PulseAudio.Volume.INVALID; + + protected virtual void preshow () {} + + private void activate () { + preshow (); + _ok_volume = multimedia_volume; + + _notification.show (); + this.active = true; + + // lower the volume to just the warning level + // from the sound specs: + // "Whenever you increase volume,..., such that acoustic output would be MORE than 85 dB + sound_system_set_multimedia_volume (_options.loud_volume); + } + + private void on_user_response (IndicatorSound.WarnNotification.Response response) { + + this.active = false; + + if (response == IndicatorSound.WarnNotification.Response.OK) { + approve_high_volume (); + sound_system_set_multimedia_volume (_ok_volume); + } else { + this.cancel_pressed (VolumeWarning.volume_to_double(_options.loud_volume)); + } + + _ok_volume = PulseAudio.Volume.INVALID; + } + + private static double volume_to_double (PulseAudio.Volume vol) + { + double tmp = (double)(vol - PulseAudio.Volume.MUTED); + return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED); + } + + public signal void cancel_pressed (double cancel_volume); } |