diff options
-rw-r--r-- | data/com.canonical.indicator.sound.gschema.xml | 39 | ||||
-rw-r--r-- | src/service.vala | 192 | ||||
-rw-r--r-- | src/volume-control-pulse.vala | 10 |
3 files changed, 162 insertions, 79 deletions
diff --git a/data/com.canonical.indicator.sound.gschema.xml b/data/com.canonical.indicator.sound.gschema.xml index a346c0d..ff4816e 100644 --- a/data/com.canonical.indicator.sound.gschema.xml +++ b/data/com.canonical.indicator.sound.gschema.xml @@ -39,5 +39,44 @@ Whether or not to show the sound indicator in the menu bar. </description> </key> + + <!-- VOLUME --> + + <key name="high-volume-warning-enabled" type="b"> + <default>true</default> + <summary>Whether or not to show the a volume warning.</summary> + <description> + Whether or not to show the a volume warning when the volume exceeds some level while headphones are plugged in. + </description> + </key> + <key name="high-volume-acknowledgment-ttl" type="i"> + <default>1200</default> + <summary>How often, in hours, a user's high volume confirmation should be remembered.</summary> + <description> + After a user confirms that they want to listen at a higher volume, subsequent volume + changes do not need to re-trigger a warning until this interval has passed. + For example, EU standard EN 60950-1/Al2 cites "The acknowledgement does not need to + be repeated more than once every 20 h of cumulative listening time." + </description> + </key> + <key name="high-volume-level" type="d"> + <default>0.75</default> + <summary>Volume level that triggers a high volume warning. [0.0..1.0]</summary><!-- FIXME: decibels would be better --> + <description> + When high volume warnings are enabled, a warning will be shown when + the volume level is raised past this level. + </description> + </key> +<!-- FIXME: not used yet, needs to be worked into the service.max_volume property wrt allow_amplified_volume . + Also, decibels would be better here + <key name="maximum-volume" type="d"> + <default>1.0</default> + <summary>Maximum volume level, [0.0..1.0]</summary> + <description> + Maximum volume level, [0.0..1.0] + </description> + </key> +--> + </schema> </schemalist> diff --git a/src/service.vala b/src/service.vala index 22be11d..baecd09 100644 --- a/src/service.vala +++ b/src/service.vala @@ -27,11 +27,24 @@ public class IndicatorSound.Service: Object { error("Unable to get DBus session bus: %s", e.message); } - sync_notification = new Notify.Notification(_("Volume"), "", "audio-volume-muted"); + info_notification = new Notify.Notification(_("Volume"), "", "audio-volume-muted"); + + warn_notification = new Notify.Notification(_("Volume"), _("High volume can damage your hearing."), "audio-volume-high"); + warn_notification.set_hint ("x-canonical-non-shaped-icon", "true"); + warn_notification.set_hint ("x-canonical-snap-decisions", "true"); + warn_notification.set_hint ("x-canonical-private-affirmative-tint", "true"); + warn_notification.add_action ("ok", _("OK"), (n, a) => { + this.loudness_approved_timestamp = GLib.get_monotonic_time (); + }); + warn_notification.add_action ("cancel", _("Cancel"), (n, a) => { + /* user rejected loud volume; re-clamp to just below the warning level */ + set_clamped_volume (settings.get_double("high-volume-level") * 0.9); + }); + BusWatcher.watch_namespace (GLib.BusType.SESSION, "org.freedesktop.Notifications", - () => { debug("Notifications name appeared"); check_sync_notification = false; }, - () => { debug("Notifications name vanshed"); check_sync_notification = false; }); + () => { debug("Notifications name appeared"); notify_server_caps_checked = false; }, + () => { debug("Notifications name vanshed"); notify_server_caps_checked = false; }); this.settings = new Settings ("com.canonical.indicator.sound"); this.sharedsettings = new Settings ("com.ubuntu.sound"); @@ -88,14 +101,10 @@ public class IndicatorSound.Service: Object { /* Hide the notification when the menu is shown */ var shown_action = actions.lookup_action ("indicator-shown") as SimpleAction; shown_action.change_state.connect ((state) => { - block_notifications = state.get_boolean(); - if (block_notifications) { + block_info_notifications = state.get_boolean(); + if (block_info_notifications) { debug("Indicator is shown"); - try { - sync_notification.close(); - } catch (Error e) { - warning("Unable to close synchronous volume notification: %s", e.message); - } + close_notification(info_notification); } else { debug("Indicator is hidden"); } @@ -111,6 +120,26 @@ public class IndicatorSound.Service: Object { this.menus.@foreach ( (profile, menu) => menu.export (bus, @"/com/canonical/indicator/sound/$profile")); } + private void close_notification(Notify.Notification? n) { + if ((n != null) && (n.id != 0)) { + try { + n.close(); + } catch (GLib.Error e) { + warning("Unable to close notification: %s", e.message); + } + } + } + + private void show_notification(Notify.Notification? n) { + if (n != null) { + try { + n.show (); + } catch (GLib.Error e) { + warning ("Unable to show notification: %s", e.message); + } + } + } + ~Service() { debug("Destroying Service Object"); @@ -187,7 +216,8 @@ public class IndicatorSound.Service: Object { bool syncing_preferred_players = false; AccountsServiceUser? accounts_service = null; bool export_to_accounts_service = false; - private Notify.Notification sync_notification; + private Notify.Notification info_notification; + private Notify.Notification warn_notification; /* Maximum volume as a scaling factor between the volume action's state and the value in * this.volume_control. See create_volume_action(). @@ -196,14 +226,17 @@ public class IndicatorSound.Service: Object { const double volume_step_percentage = 0.06; + void set_clamped_volume (double unclamped) { + var vol = new VolumeControl.Volume(); + vol.volume = unclamped.clamp (0.0, this.max_volume); + vol.reason = VolumeControl.VolumeReasons.USER_KEYPRESS; + this.volume_control.volume = vol; + } + void activate_scroll_action (SimpleAction action, Variant? param) { int delta = param.get_int32(); /* positive for up, negative for down */ - - var scrollvol = new VolumeControl.Volume(); double v = this.volume_control.volume.volume + volume_step_percentage * delta; - scrollvol.volume = v.clamp (0.0, this.max_volume); - scrollvol.reason = VolumeControl.VolumeReasons.USER_KEYPRESS; - this.volume_control.volume = scrollvol; + set_clamped_volume(v); } void activate_desktop_settings (SimpleAction action, Variant? param) { @@ -275,60 +308,71 @@ public class IndicatorSound.Service: Object { root_action.set_state (builder.end()); } - private bool check_sync_notification = false; - private bool support_sync_notification = false; - private bool block_notifications = false; + private bool notify_server_caps_checked = false; + private bool notify_server_supports_actions = false; + private bool notify_server_supports_sync = false; + private bool block_info_notifications = false; + private int64 loudness_approved_timestamp = 0; + + bool user_recently_approved_loudness() { + int64 ttl_sec = this.settings.get_int("high-volume-acknowledgment-ttl"); + int64 ttl_usec = ttl_sec * 1000000; + int64 oldest_time_allowed = GLib.get_monotonic_time() - ttl_usec; + return this.loudness_approved_timestamp >= oldest_time_allowed; + } + + void update_notification () { - void update_sync_notification () { - if (!check_sync_notification) { - support_sync_notification = false; + if (!notify_server_caps_checked) { List<string> caps = Notify.get_server_caps (); - if (caps.find_custom ("x-canonical-private-synchronous", strcmp) != null) { - support_sync_notification = true; - } - check_sync_notification = true; + notify_server_supports_actions = caps.find_custom ("actions", strcmp) != null; + notify_server_supports_sync = caps.find_custom ("x-canonical-private-synchronous", strcmp) != null; + notify_server_caps_checked = true; } - if (!support_sync_notification) - return; - - if (block_notifications) - return; - - /* Determine Label */ - string volume_label = ""; - if (volume_control.high_volume) - volume_label = _("High volume"); - - /* Choose an icon */ - string icon = "audio-volume-muted"; - if (volume_control.volume.volume <= 0.0) - icon = "audio-volume-muted"; - else if (volume_control.volume.volume <= 0.3) - icon = "audio-volume-low"; - else if (volume_control.volume.volume <= 0.7) - icon = "audio-volume-medium"; - else - icon = "audio-volume-high"; - - /* Check tint */ - string tint = "false"; - if (volume_control.high_volume) - tint = "true"; - - /* Put it all into the notification */ - sync_notification.clear_hints (); - sync_notification.update (_("Volume"), volume_label, icon); - sync_notification.set_hint ("value", (int32)Math.round(volume_control.volume.volume / this.max_volume * 100.0)); - sync_notification.set_hint ("x-canonical-value-bar-tint", tint); - sync_notification.set_hint ("x-canonical-private-synchronous", "true"); - sync_notification.set_hint ("x-canonical-non-shaped-icon", "true"); - - /* Show it */ - try { - sync_notification.show (); - } catch (GLib.Error e) { - warning("Unable to send volume change notification: %s", e.message); + var loud = volume_control.high_volume; + var warn = loud + && this.notify_server_supports_actions + && this.settings.get_boolean("high-volume-warning-enabled") + && !this.user_recently_approved_loudness(); + + if (warn) { + close_notification(info_notification); + show_notification(warn_notification); + } else { + close_notification(warn_notification); + + if (notify_server_supports_sync && !block_info_notifications) { + + /* Determine Label */ + string volume_label = ""; + if (loud) { + volume_label = _("High volume can damage your hearing."); + } + + /* Choose an icon */ + string icon = ""; + if (loud) + icon = "audio-volume-high"; + else if (volume_control.volume.volume <= 0.0) + icon = "audio-volume-muted"; + else if (volume_control.volume.volume <= 0.3) + icon = "audio-volume-low"; + else if (volume_control.volume.volume <= 0.7) + icon = "audio-volume-medium"; + else + icon = "audio-volume-high"; + + /* Reset the notification */ + var n = this.info_notification; + n.update (_("Volume"), volume_label, icon); + n.clear_hints(); + n.set_hint ("x-canonical-non-shaped-icon", "true"); + n.set_hint ("x-canonical-private-synchronous", "true"); + n.set_hint ("x-canonical-value-bar-tint", loud ? "true" : "false"); + n.set_hint ("value", (int32)Math.round(volume_control.volume.volume / this.max_volume * 100.0)); + show_notification(n); + } } } @@ -423,22 +467,14 @@ public class IndicatorSound.Service: Object { volume_action.change_state.connect ( (action, val) => { double v = val.get_double () * this.max_volume; - - var vol = new VolumeControl.Volume(); - vol.volume = v.clamp (0.0, this.max_volume); - vol.reason = VolumeControl.VolumeReasons.USER_KEYPRESS; - volume_control.volume = vol; + set_clamped_volume(v); }); /* activating this action changes the volume by the amount given in the parameter */ volume_action.activate.connect ( (action, param) => { int delta = param.get_int32 (); double v = volume_control.volume.volume + volume_step_percentage * delta; - - var vol = new VolumeControl.Volume(); - vol.volume = v.clamp (0.0, this.max_volume); - vol.reason = VolumeControl.VolumeReasons.USER_KEYPRESS; - volume_control.volume = vol; + set_clamped_volume(v); }); this.volume_control.notify["volume"].connect (() => { @@ -450,7 +486,7 @@ public class IndicatorSound.Service: Object { var reason = volume_control.volume.reason; if (reason == VolumeControl.VolumeReasons.USER_KEYPRESS || reason == VolumeControl.VolumeReasons.DEVICE_OUTPUT_CHANGE) - this.update_sync_notification (); + this.update_notification (); }); this.volume_control.bind_property ("ready", volume_action, "enabled", BindingFlags.SYNC_CREATE); @@ -481,7 +517,7 @@ public class IndicatorSound.Service: Object { this.volume_control.notify["high-volume"].connect( () => { high_volume_action.set_state(new Variant.boolean (this.volume_control.high_volume)); - update_sync_notification(); + update_notification(); }); return high_volume_action; diff --git a/src/volume-control-pulse.vala b/src/volume-control-pulse.vala index 3d4d113..d3e93c5 100644 --- a/src/volume-control-pulse.vala +++ b/src/volume-control-pulse.vala @@ -44,6 +44,7 @@ public class VolumeControlPulse : VolumeControl private bool _is_playing = false; private VolumeControl.Volume _volume = new VolumeControl.Volume(); private double _mic_volume = 0.0; + private Settings _settings = new Settings ("com.canonical.indicator.sound"); /* Used by the pulseaudio stream restore extension */ private DBusConnection _pconn; @@ -95,7 +96,14 @@ public class VolumeControlPulse : VolumeControl /** true when high volume warnings should be shown */ public override bool high_volume { get { - return this._volume.volume > 0.75 && _active_port_headphone && stream == "multimedia"; + if (!_active_port_headphone) { + return false; + } + if (stream != "multimedia") { + return false; + } + var high_volume_level = this._settings.get_double("high-volume-level"); + return this._volume.volume > high_volume_level; } } |