diff options
author | charles kerr <charlesk@canonical.com> | 2016-01-13 20:37:55 +0000 |
---|---|---|
committer | CI Train Bot <ci-train-bot@canonical.com> | 2016-01-13 20:37:55 +0000 |
commit | e43001712810b56a1579545ecafd9f9826e7a438 (patch) | |
tree | 068221198bc1ca78180ac7106d2303cbc4e3079e | |
parent | 9a861c0911ce8345f48909993249c8a74d8bfb2c (diff) | |
parent | 9b72c7f662440844bf59521e124f59bcbddec4a5 (diff) | |
download | ayatana-indicator-sound-e43001712810b56a1579545ecafd9f9826e7a438.tar.gz ayatana-indicator-sound-e43001712810b56a1579545ecafd9f9826e7a438.tar.bz2 ayatana-indicator-sound-e43001712810b56a1579545ecafd9f9826e7a438.zip |
Be more selective about when to show and dismiss the High Volume Warning Dialog. Fixes: #1504065
Approved by: PS Jenkins bot, Xavi Garcia
31 files changed, 1435 insertions, 862 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index c1fbb8d..16ef27f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,12 +24,14 @@ set(SOURCE_DIR "${CMAKE_SOURCE_DIR}/src") set(SOURCE_BINARY_DIR "${CMAKE_BINARY_DIR}/src") set(PULSE_AUDIO_REQUIRED_VERSION 0.9.19) +set(GLIB_2_0_REQUIRED_VERSION 2.32) set(GIO_2_0_REQUIRED_VERSION 2.25.13) set(URL_DISPATCHER_1_REQUIRED_VERSION 1) pkg_check_modules( PULSEAUDIO REQUIRED libpulse-mainloop-glib>=${PULSE_AUDIO_REQUIRED_VERSION} + glib-2.0>=${GLIB_2_0_REQUIRED_VERSION} gio-unix-2.0>=${GIO_2_0_REQUIRED_VERSION} url-dispatcher-1>=${URL_DISPATCHER_1_REQUIRED_VERSION} ) diff --git a/debian/control b/debian/control index f170fcf..e65a858 100644 --- a/debian/control +++ b/debian/control @@ -18,7 +18,7 @@ Build-Depends: debhelper (>= 9.0), libaccountsservice-dev, libdbustest1-dev (>= 15.04.0), libgirepository1.0-dev, - libglib2.0-dev (>= 2.22.3), + libglib2.0-dev (>= 2.32.0), libgtest-dev, libqtdbusmock1-dev (>= 0.3), libqtdbustest1-dev, diff --git a/po/POTFILES.in b/po/POTFILES.in index 4bd8cdf..e5fefb7 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,3 +1,7 @@ [encoding: UTF-8] +src/info-notification.vala +src/options-gsettings.vala src/service.vala src/sound-menu.vala +src/warn-notification.vala + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 73a270c..57bf539 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,6 +32,7 @@ vala_init(indicator-sound-service OPTIONS --ccode --thread + --target-glib=${GLIB_2_0_REQUIRED_VERSION} --vapidir=${CMAKE_SOURCE_DIR}/vapi/ --vapidir=. --pkg=url-dispatcher @@ -39,23 +40,79 @@ vala_init(indicator-sound-service ) vala_add(indicator-sound-service + notification.vala +) +vala_add(indicator-sound-service + info-notification.vala + DEPENDS + notification + volume-control + options +) +vala_add(indicator-sound-service + warn-notification.vala + DEPENDS + notification +) +vala_add(indicator-sound-service service.vala DEPENDS sound-menu volume-control volume-control-pulse + notification + info-notification + volume-warning + options + options-gsettings media-player media-player-list mpris2-interfaces accounts-service-user ) vala_add(indicator-sound-service + options.vala + DEPENDS + volume-control + volume-control-pulse +) +vala_add(indicator-sound-service + options-gsettings.vala + DEPENDS + options + volume-control-pulse + volume-control +) +vala_add(indicator-sound-service volume-control.vala + DEPENDS + options + volume-control-pulse ) vala_add(indicator-sound-service volume-control-pulse.vala DEPENDS + options + volume-control +) +vala_add(indicator-sound-service + volume-warning.vala + DEPENDS + options + volume-control-pulse volume-control + warn-notification + notification +) +vala_add(indicator-sound-service + volume-warning-pulse.vala + DEPENDS + volume-warning + options + volume-control-pulse + volume-control + warn-notification + notification ) vala_add(indicator-sound-service media-player.vala @@ -104,6 +161,8 @@ vala_add(indicator-sound-service DEPENDS media-player volume-control + options + volume-control-pulse ) vala_add(indicator-sound-service accounts-service-user.vala diff --git a/src/Makefile.am.THIS b/src/Makefile.am.THIS deleted file mode 100644 index 1a82a18..0000000 --- a/src/Makefile.am.THIS +++ /dev/null @@ -1,39 +0,0 @@ -pkglibexec_PROGRAMS = indicator-sound-service - -indicator_sound_service_SOURCES = \ - service.vala \ - main.vala \ - volume-control.vala \ - media-player.vala \ - media-player-list.vala \ - mpris2-interfaces.vala \ - freedesktop-interfaces.vala \ - sound-menu.vala \ - bus-watch-namespace.c \ - bus-watch-namespace.h - -indicator_sound_service_VALAFLAGS = \ - --ccode \ - --vapidir=$(top_srcdir)/vapi/ \ - --vapidir=./ \ - --thread \ - --pkg config \ - --pkg gio-2.0 \ - --pkg gio-unix-2.0 \ - --pkg libxml-2.0 \ - --pkg libpulse \ - --pkg libpulse-mainloop-glib \ - --pkg bus-watcher \ - --target-glib=2.36 - -# -w to disable warnings for vala-generated code -indicator_sound_service_CFLAGS = $(PULSEAUDIO_CFLAGS) \ - $(SOUNDSERVICE_CFLAGS) \ - $(GCONF_CFLAGS) \ - $(COVERAGE_CFLAGS) \ - -DLIBEXECDIR=\"$(libexecdir)\" \ - -w \ - -DGETTEXT_PACKAGE=\"$(GETTEXT_PACKAGE)\" - -indicator_sound_service_LDADD = $(PULSEAUDIO_LIBS) $(SOUNDSERVICE_LIBS) $(GCONF_LIBS) -indicator_sound_service_LDFLAGS = $(COVERAGE_LDFLAGS) diff --git a/src/accounts-service-user.vala b/src/accounts-service-user.vala index e8db7c4..1f9dcce 100644 --- a/src/accounts-service-user.vala +++ b/src/accounts-service-user.vala @@ -185,7 +185,7 @@ public class AccountsServiceUser : Object { this.privacyproxy = Bus.get_proxy.end (res); (this.privacyproxy as DBusProxy).g_properties_changed.connect((proxy, changed, invalid) => { - var welcomeval = changed.lookup_value("MessagesWelcomeScreen", new VariantType("b")); + var welcomeval = changed.lookup_value("MessagesWelcomeScreen", VariantType.BOOLEAN); if (welcomeval != null) { debug("Messages on welcome screen changed"); this.showDataOnGreeter = welcomeval.get_boolean(); @@ -204,7 +204,7 @@ public class AccountsServiceUser : Object { this.syssoundproxy = Bus.get_proxy.end (res); (this.syssoundproxy as DBusProxy).g_properties_changed.connect((proxy, changed, invalid) => { - var silentvar = changed.lookup_value("SilentMode", new VariantType("b")); + var silentvar = changed.lookup_value("SilentMode", VariantType.BOOLEAN); if (silentvar != null) { debug("Silent Mode changed"); this._silentMode = silentvar.get_boolean(); diff --git a/src/info-notification.vala b/src/info-notification.vala new file mode 100644 index 0000000..2ce8ef6 --- /dev/null +++ b/src/info-notification.vala @@ -0,0 +1,123 @@ +/* + * Copyright 2015 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +using Notify; + +public class IndicatorSound.InfoNotification: Notification +{ + protected override Notify.Notification create_notification () { + return new Notify.Notification (_("Volume"), "", "audio-volume-muted"); + } + + public void show (VolumeControl.ActiveOutput active_output, + double volume, + bool is_high_volume) { + if (!notify_server_supports ("x-canonical-private-synchronous")) + return; + + /* Determine Label */ + unowned string volume_label = get_notification_label (active_output); + + /* Choose an icon */ + unowned string icon = get_volume_notification_icon (active_output, volume, is_high_volume); + + /* Reset the notification */ + var n = _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", is_high_volume ? "true" : "false"); + n.set_hint ("value", ((int32)((volume * 100.0) + 0.5)).clamp(0, 100)); + show_notification (); + } + + private static unowned string get_notification_label (VolumeControl.ActiveOutput active_output) { + + switch (active_output) { + case VolumeControl.ActiveOutput.SPEAKERS: + return _("Speakers"); + case VolumeControl.ActiveOutput.HEADPHONES: + return _("Headphones"); + case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: + return _("Bluetooth headphones"); + case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: + return _("Bluetooth speaker"); + case VolumeControl.ActiveOutput.USB_SPEAKER: + return _("Usb speaker"); + case VolumeControl.ActiveOutput.USB_HEADPHONES: + return _("Usb headphones"); + case VolumeControl.ActiveOutput.HDMI_SPEAKER: + return _("HDMI speaker"); + case VolumeControl.ActiveOutput.HDMI_HEADPHONES: + return _("HDMI headphones"); + default: + return ""; + } + } + + private static unowned string get_volume_notification_icon (VolumeControl.ActiveOutput active_output, + double volume, + bool is_high_volume) { + + if (!is_high_volume) + return get_volume_icon (active_output, volume); + + switch (active_output) { + case VolumeControl.ActiveOutput.SPEAKERS: + case VolumeControl.ActiveOutput.HEADPHONES: + case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: + case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: + case VolumeControl.ActiveOutput.USB_SPEAKER: + case VolumeControl.ActiveOutput.USB_HEADPHONES: + case VolumeControl.ActiveOutput.HDMI_SPEAKER: + case VolumeControl.ActiveOutput.HDMI_HEADPHONES: + return "audio-volume-high"; + + default: + return ""; + } + } + + private static unowned string get_volume_icon (VolumeControl.ActiveOutput active_output, + double volume) + { + switch (active_output) { + case VolumeControl.ActiveOutput.SPEAKERS: + case VolumeControl.ActiveOutput.HEADPHONES: + case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: + case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: + case VolumeControl.ActiveOutput.USB_SPEAKER: + case VolumeControl.ActiveOutput.USB_HEADPHONES: + case VolumeControl.ActiveOutput.HDMI_SPEAKER: + case VolumeControl.ActiveOutput.HDMI_HEADPHONES: + if (volume <= 0.0) + return "audio-volume-muted"; + if (volume <= 0.3) + return "audio-volume-low"; + if (volume <= 0.7) + return "audio-volume-medium"; + return "audio-volume-high"; + + default: + return ""; + } + } +} + @@ -22,6 +22,7 @@ #include "config.h" static IndicatorSoundService * service = NULL; +static pa_glib_mainloop * pgloop = NULL; static gboolean sigterm_handler (gpointer data) @@ -46,8 +47,10 @@ on_bus_acquired(GDBusConnection *connection, gpointer user_data) { MediaPlayerList * playerlist = NULL; + IndicatorSoundOptions * options = NULL; VolumeControlPulse * volume = NULL; AccountsServiceUser * accounts = NULL; + VolumeWarning * warning = NULL; if (g_strcmp0("lightdm", g_get_user_name()) == 0) { @@ -57,20 +60,24 @@ on_bus_acquired(GDBusConnection *connection, accounts = accounts_service_user_new(); } - volume = volume_control_pulse_new(); + pgloop = pa_glib_mainloop_new(NULL); + options = indicator_sound_options_gsettings_new(); + volume = volume_control_pulse_new(options, pgloop); + warning = volume_warning_pulse_new(options, pgloop); - service = indicator_sound_service_new (playerlist, volume, accounts); + service = indicator_sound_service_new (playerlist, volume, accounts, options, warning); g_clear_object(&playerlist); + g_clear_object(&options); g_clear_object(&volume); g_clear_object(&accounts); + g_clear_object(&warning); } int main (int argc, char ** argv) { GMainLoop * loop = NULL; - bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); setlocale (LC_ALL, ""); bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR); @@ -95,6 +102,7 @@ main (int argc, char ** argv) g_main_loop_run(loop); g_clear_object(&service); + g_clear_pointer(&pgloop, pa_glib_mainloop_free); notify_uninit(); diff --git a/src/media-player-mpris.vala b/src/media-player-mpris.vala index 408cdda..93ce34e 100644 --- a/src/media-player-mpris.vala +++ b/src/media-player-mpris.vala @@ -226,7 +226,7 @@ public class MediaPlayerMpris: MediaPlayer { 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 false; } ); + Timeout.add (1000, () => { proxy.PlayPause.begin (); return Source.REMOVE; } ); this.play_when_attached = false; } } @@ -269,7 +269,7 @@ public class MediaPlayerMpris: MediaPlayer { return; } - Timeout.add (500, () => { this.fetch_playlists (); return false; } ); + 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 @@ -295,7 +295,7 @@ public class MediaPlayerMpris: MediaPlayer { this.playbackstatus_changed (); } - var metadata = changed_properties.lookup_value ("Metadata", new VariantType ("a{sv}")); + var metadata = changed_properties.lookup_value ("Metadata", VariantType.VARDICT); if (metadata != null) this.update_current_track (metadata); } diff --git a/src/media-player-user.vala b/src/media-player-user.vala index 11678d5..1be1a18 100644 --- a/src/media-player-user.vala +++ b/src/media-player-user.vala @@ -75,8 +75,7 @@ public class MediaPlayerUser : MediaPlayer { properties_queued.remove_all(); - /* Remove source */ - return false; + return Source.REMOVE; } /* Turns the DBus names into the object properties */ diff --git a/src/mpris2-interfaces.vala b/src/mpris2-interfaces.vala index 0ed8719..f9060af 100644 --- a/src/mpris2-interfaces.vala +++ b/src/mpris2-interfaces.vala @@ -1,18 +1,18 @@ /* -Copyright 2010 Canonical Ltd. +Copyright 2010-2015 Canonical Ltd. Authors: Conor Curran <conor.curran@canonical.com> -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/>. */ @@ -36,8 +36,8 @@ public interface MprisRoot : Object { public interface MprisPlayer : Object { // properties public abstract HashTable<string, Variant?> Metadata{owned get; set;} - public abstract int32 Position{owned get; set;} - public abstract string? PlaybackStatus{owned get; set;} + public abstract int64 Position{owned get; set;} + public abstract string? PlaybackStatus{owned get; set;} public abstract bool CanPlay{owned get; set;} public abstract bool CanGoNext{owned get; set;} public abstract bool CanGoPrevious{owned get; set;} diff --git a/src/notification.vala b/src/notification.vala new file mode 100644 index 0000000..3a3060f --- /dev/null +++ b/src/notification.vala @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +public abstract class IndicatorSound.Notification: Object +{ + public Notification () { + BusWatcher.watch_namespace ( + GLib.BusType.SESSION, + "org.freedesktop.Notifications", + () => { debug ("Notifications name appeared"); }, + () => { debug ("Notifications name vanshed"); _server_caps = null; }); + + _notification = create_notification (); + } + + public void close () { + var n = _notification; + + return_if_fail (n != null); + + if (n.id != 0) { + try { + n.close (); + } catch (GLib.Error e) { + GLib.warning ("Unable to close notification: %s", e.message); + } + } + } + + ~Notification () { + close (); + } + + protected abstract Notify.Notification create_notification (); + + protected void show_notification () { + try { + _notification.show (); + } catch (GLib.Error e) { + GLib.warning ("Unable to show notification: %s", e.message); + } + } + + protected bool notify_server_supports (string cap) { + if (_server_caps == null) + _server_caps = Notify.get_server_caps (); + + return _server_caps.find_custom (cap, strcmp) != null; + } + + protected Notify.Notification _notification = null; + + private static List<string> _server_caps = null; +} diff --git a/src/options-gsettings.vala b/src/options-gsettings.vala new file mode 100644 index 0000000..85fdc66 --- /dev/null +++ b/src/options-gsettings.vala @@ -0,0 +1,87 @@ +/* + * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*- + * Copyright 2015 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +using PulseAudio; + +public class IndicatorSound.OptionsGSettings : Options +{ + public OptionsGSettings() { + init_max_volume(); + init_loud_volume(); + } + + ~OptionsGSettings() { + } + + private Settings _settings = new Settings ("com.canonical.indicator.sound"); + private Settings _shared_settings = new Settings ("com.ubuntu.sound"); + + /** MAX VOLUME PROPERTY **/ + + private static const string AMP_dB_KEY = "amplified-volume-decibels"; + private static const string NORMAL_dB_KEY = "normal-volume-decibels"; + private static const string ALLOW_AMP_KEY = "allow-amplified-volume"; + + private void init_max_volume() { + _settings.changed[NORMAL_dB_KEY].connect(() => update_max_volume()); + _settings.changed[AMP_dB_KEY].connect(() => update_max_volume()); + _shared_settings.changed[ALLOW_AMP_KEY].connect(() => update_max_volume()); + update_max_volume(); + } + private void update_max_volume () { + set_max_volume_(calculate_max_volume()); + } + protected void set_max_volume_ (double vol) { + if (max_volume != vol) { + debug("changing max_volume from %f to %f", this.max_volume, vol); + max_volume = vol; + } + } + private double calculate_max_volume () { + unowned string decibel_key = _shared_settings.get_boolean(ALLOW_AMP_KEY) + ? AMP_dB_KEY + : NORMAL_dB_KEY; + var volume_dB = _settings.get_double(decibel_key); + var volume_sw = PulseAudio.Volume.sw_from_dB (volume_dB); + return VolumeControlPulse.volume_to_double (volume_sw); + } + + + /** LOUD VOLUME **/ + + private static const string LOUD_ENABLED_KEY = "warning-volume-enabled"; + private static const string LOUD_DECIBEL_KEY = "warning-volume-decibels"; + + private void init_loud_volume() { + _settings.changed[LOUD_ENABLED_KEY].connect(() => update_loud_volume()); + _settings.changed[LOUD_DECIBEL_KEY].connect(() => update_loud_volume()); + update_loud_volume(); + } + private void update_loud_volume() { + + var vol = PulseAudio.Volume.sw_from_dB (_settings.get_double (LOUD_DECIBEL_KEY)); + if (loud_volume != vol) + loud_volume = vol; + + var enabled = _settings.get_boolean(LOUD_ENABLED_KEY); + if (loud_warning_enabled != enabled) + loud_warning_enabled = enabled; + } +} diff --git a/src/options.vala b/src/options.vala new file mode 100644 index 0000000..1aab852 --- /dev/null +++ b/src/options.vala @@ -0,0 +1,28 @@ +/* + * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*- + * Copyright 2015 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +public abstract class IndicatorSound.Options : Object +{ + public double max_volume { get; protected set; default = 1.0; } + + public uint loud_volume { get; protected set; default = PulseAudio.Volume.sw_from_dB(8); } + + public bool loud_warning_enabled { get; protected set; default = true; } +} diff --git a/src/service.vala b/src/service.vala index 0a7e108..29b8670 100644 --- a/src/service.vala +++ b/src/service.vala @@ -20,35 +20,25 @@ public class IndicatorSound.Service: Object { DBusConnection bus; - /** - * A copy of volume_control.volume made before just warn_notification - * is shown. Since the volume is clamped during the warning, we cache - * the previous volume to use iff the user hits "OK". - */ - VolumeControl.Volume _pre_warn_volume = null; - - public Service (MediaPlayerList playerlist, VolumeControl volume, AccountsServiceUser? accounts) { + public Service (MediaPlayerList playerlist, VolumeControl volume, AccountsServiceUser? accounts, Options options, VolumeWarning volume_warning) { + try { bus = Bus.get_sync(GLib.BusType.SESSION); } catch (GLib.Error e) { error("Unable to get DBus session bus: %s", e.message); } - info_notification = new Notify.Notification(_("Volume"), "", "audio-volume-muted"); + _options = options; + _options.notify["max-volume"].connect(() => { + update_volume_action_state(); + this.update_notification(); + }); - 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.closed.connect((n) => { - n.clear_actions(); - waiting_user_approve_warn=false; - increment_volume_sync_action(); + _volume_warning = volume_warning; + _volume_warning.notify["active"].connect(() => { + this.increment_volume_sync_action(); + this.update_notification(); }); - BusWatcher.watch_namespace (GLib.BusType.SESSION, - "org.freedesktop.Notifications", - () => { debug("Notifications name appeared"); }, - () => { debug("Notifications name vanshed"); notify_server_caps_checked = false; }); this.settings = new Settings ("com.canonical.indicator.sound"); @@ -56,8 +46,26 @@ public class IndicatorSound.Service: Object { this.notify["visible"].connect ( () => this.update_root_icon () ); this.volume_control = volume; - this.volume_control.active_output_changed.connect (this.update_root_icon); - this.volume_control.active_output_changed.connect (this.update_notification); + this.volume_control.active_output_changed.connect(() => { + bool headphones; + switch(volume_control.active_output()) { + case VolumeControl.ActiveOutput.HEADPHONES: + case VolumeControl.ActiveOutput.USB_HEADPHONES: + case VolumeControl.ActiveOutput.HDMI_HEADPHONES: + case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: + headphones = true; + break; + + default: + headphones = false; + break; + } + message("setting _volume_warning.headphones_active to %d", (int)headphones); + _volume_warning.headphones_active = headphones; + + update_root_icon(); + update_notification(); + }); this.accounts_service = accounts; /* If we're on the greeter, don't export */ @@ -94,7 +102,7 @@ public class IndicatorSound.Service: Object { }); this.menus.@foreach ( (profile, menu) => { - this.volume_control.bind_property ("high-volume", menu, "show-high-volume-warning", BindingFlags.SYNC_CREATE); + _volume_warning.bind_property ("high-volume", menu, "show-high-volume-warning", BindingFlags.SYNC_CREATE); }); this.menus.@foreach ( (profile, menu) => { @@ -112,7 +120,7 @@ public class IndicatorSound.Service: Object { block_info_notifications = state.get_boolean(); if (block_info_notifications) { debug("Indicator is shown"); - close_notification(info_notification); + _info_notification.close(); } else { debug("Indicator is hidden"); } @@ -128,33 +136,11 @@ 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) { - return_if_fail (n != null); - if (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) { - return_if_fail (n != null); - try { - n.show (); - } catch (GLib.Error e) { - warning ("Unable to show notification: %s", e.message); - } - } - ~Service() { debug("Destroying Service Object"); clear_acts_player(); - stop_clamp_to_high_timeout(); - if (this.player_action_update_id > 0) { Source.remove (this.player_action_update_id); this.player_action_update_id = 0; @@ -203,19 +189,29 @@ public class IndicatorSound.Service: Object { bool syncing_preferred_players = false; AccountsServiceUser? accounts_service = null; bool export_to_accounts_service = false; - private Notify.Notification info_notification; - private Notify.Notification warn_notification; + private Options _options; + private VolumeWarning _volume_warning; + private IndicatorSound.InfoNotification _info_notification = new IndicatorSound.InfoNotification(); const double volume_step_percentage = 0.06; private void activate_scroll_action (SimpleAction action, Variant? param) { - int delta = param.get_int32(); /* positive for up, negative for down */ - double v = volume_control.volume.volume + volume_step_percentage * delta; - volume_control.set_volume_clamp (v, VolumeControl.VolumeReasons.USER_KEYPRESS); + int direction = param.get_int32(); // positive for up, negative for down + message("scroll: %d", direction); + + if (_volume_warning.active) { + _volume_warning.user_keypress(direction>0 + ? VolumeWarning.Key.VOLUME_UP + : VolumeWarning.Key.VOLUME_DOWN); + } else { + double delta = volume_step_percentage * direction; + double v = volume_control.volume.volume + delta; + volume_control.set_volume_clamp (v, VolumeControl.VolumeReasons.USER_KEYPRESS); + } } void activate_desktop_settings (SimpleAction action, Variant? param) { - var env = Environment.get_variable ("DESKTOP_SESSION"); + unowned string env = Environment.get_variable ("DESKTOP_SESSION"); string cmd; if (Environment.get_variable ("MIR_SOCKET") != null) @@ -256,7 +252,7 @@ public class IndicatorSound.Service: Object { void update_root_icon () { double volume = this.volume_control.volume.volume; - string icon = get_volume_root_icon (volume, this.volume_control.mute, volume_control.active_output); + unowned string icon = get_volume_root_icon (volume, this.volume_control.mute, volume_control.active_output()); string accessible_name; if (this.volume_control.mute) { @@ -270,7 +266,7 @@ public class IndicatorSound.Service: Object { } var root_action = actions.lookup_action ("root") as SimpleAction; - var builder = new VariantBuilder (new VariantType ("a{sv}")); + var builder = new VariantBuilder (VariantType.VARDICT); builder.add ("{sv}", "title", new Variant.string (_("Sound"))); builder.add ("{sv}", "accessible-desc", new Variant.string (accessible_name)); builder.add ("{sv}", "icon", serialize_themed_icon (icon)); @@ -278,397 +274,57 @@ public class IndicatorSound.Service: Object { root_action.set_state (builder.end()); } - 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 bool waiting_user_approve_warn = false; - private string get_volume_icon (double volume, VolumeControl.ActiveOutput active_output) - { - string icon = ""; - switch (active_output) - { + private static unowned string get_volume_root_icon_by_volume (double volume, VolumeControl.ActiveOutput active_output) { + switch (active_output) { case VolumeControl.ActiveOutput.SPEAKERS: - if (volume <= 0.0) - icon = "audio-volume-muted"; - else if (volume <= 0.3) - icon = "audio-volume-low"; - else if (volume <= 0.7) - icon = "audio-volume-medium"; - else - icon = "audio-volume-high"; - break; case VolumeControl.ActiveOutput.HEADPHONES: - if (volume <= 0.0) - icon = "audio-volume-muted"; - else if (volume <= 0.3) - icon = "audio-volume-low"; - else if (volume <= 0.7) - icon = "audio-volume-medium"; - else - icon = "audio-volume-high"; - break; case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: - if (volume <= 0.0) - icon = "audio-volume-muted"; - else if (volume <= 0.3) - icon = "audio-volume-low"; - else if (volume <= 0.7) - icon = "audio-volume-medium"; - else - icon = "audio-volume-high"; - break; case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: - if (volume <= 0.0) - icon = "audio-volume-muted"; - else if (volume <= 0.3) - icon = "audio-volume-low"; - else if (volume <= 0.7) - icon = "audio-volume-medium"; - else - icon = "audio-volume-high"; - break; case VolumeControl.ActiveOutput.USB_SPEAKER: - if (volume <= 0.0) - icon = "audio-volume-muted"; - else if (volume <= 0.3) - icon = "audio-volume-low"; - else if (volume <= 0.7) - icon = "audio-volume-medium"; - else - icon = "audio-volume-high"; - break; case VolumeControl.ActiveOutput.USB_HEADPHONES: - if (volume <= 0.0) - icon = "audio-volume-muted"; - else if (volume <= 0.3) - icon = "audio-volume-low"; - else if (volume <= 0.7) - icon = "audio-volume-medium"; - else - icon = "audio-volume-high"; - break; case VolumeControl.ActiveOutput.HDMI_SPEAKER: - if (volume <= 0.0) - icon = "audio-volume-muted"; - else if (volume <= 0.3) - icon = "audio-volume-low"; - else if (volume <= 0.7) - icon = "audio-volume-medium"; - else - icon = "audio-volume-high"; - break; case VolumeControl.ActiveOutput.HDMI_HEADPHONES: if (volume <= 0.0) - icon = "audio-volume-muted"; - else if (volume <= 0.3) - icon = "audio-volume-low"; - else if (volume <= 0.7) - icon = "audio-volume-medium"; - else - icon = "audio-volume-high"; - break; + return "audio-volume-muted-panel"; + if (volume <= 0.3) + return "audio-volume-low-panel"; + if (volume <= 0.7) + return "audio-volume-medium-panel"; + return "audio-volume-high-panel"; + + default: + return ""; } - return icon; } - private string get_volume_root_icon_by_volume (double volume, VolumeControl.ActiveOutput active_output) - { - string icon = ""; - switch (active_output) - { + private unowned string get_volume_root_icon (double volume, bool mute, VolumeControl.ActiveOutput active_output) { + switch (active_output) { case VolumeControl.ActiveOutput.SPEAKERS: - if (volume <= 0.0) - icon = "audio-volume-muted-panel"; - else if (volume <= 0.3) - icon = "audio-volume-low-panel"; - else if (volume <= 0.7) - icon = "audio-volume-medium-panel"; - else - icon = "audio-volume-high-panel"; - break; case VolumeControl.ActiveOutput.HEADPHONES: - if (volume <= 0.0) - icon = "audio-volume-muted-panel"; - else if (volume <= 0.3) - icon = "audio-volume-low-panel"; - else if (volume <= 0.7) - icon = "audio-volume-medium-panel"; - else - icon = "audio-volume-high-panel"; - break; case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: - if (volume <= 0.0) - icon = "audio-volume-muted-panel"; - else if (volume <= 0.3) - icon = "audio-volume-low-panel"; - else if (volume <= 0.7) - icon = "audio-volume-medium-panel"; - else - icon = "audio-volume-high-panel"; - break; case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: - if (volume <= 0.0) - icon = "audio-volume-muted-panel"; - else if (volume <= 0.3) - icon = "audio-volume-low-panel"; - else if (volume <= 0.7) - icon = "audio-volume-medium-panel"; - else - icon = "audio-volume-high-panel"; - break; case VolumeControl.ActiveOutput.USB_SPEAKER: - if (volume <= 0.0) - icon = "audio-volume-muted-panel"; - else if (volume <= 0.3) - icon = "audio-volume-low-panel"; - else if (volume <= 0.7) - icon = "audio-volume-medium-panel"; - else - icon = "audio-volume-high-panel"; - break; case VolumeControl.ActiveOutput.USB_HEADPHONES: - if (volume <= 0.0) - icon = "audio-volume-muted-panel"; - else if (volume <= 0.3) - icon = "audio-volume-low-panel"; - else if (volume <= 0.7) - icon = "audio-volume-medium-panel"; - else - icon = "audio-volume-high-panel"; - break; case VolumeControl.ActiveOutput.HDMI_SPEAKER: - if (volume <= 0.0) - icon = "audio-volume-muted-panel"; - else if (volume <= 0.3) - icon = "audio-volume-low-panel"; - else if (volume <= 0.7) - icon = "audio-volume-medium-panel"; - else - icon = "audio-volume-high-panel"; - break; case VolumeControl.ActiveOutput.HDMI_HEADPHONES: - if (volume <= 0.0) - icon = "audio-volume-muted-panel"; - else if (volume <= 0.3) - icon = "audio-volume-low-panel"; - else if (volume <= 0.7) - icon = "audio-volume-medium-panel"; - else - icon = "audio-volume-high-panel"; - break; - } - return icon; - } - - private string get_volume_notification_icon (double volume, bool loud, VolumeControl.ActiveOutput active_output) { - string icon = ""; - if (loud) { - switch (active_output) - { - case VolumeControl.ActiveOutput.SPEAKERS: - icon = "audio-volume-high"; - break; - case VolumeControl.ActiveOutput.HEADPHONES: - icon = "audio-volume-high"; - break; - case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: - icon = "audio-volume-high"; - break; - case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: - icon = "audio-volume-high"; - break; - case VolumeControl.ActiveOutput.USB_SPEAKER: - icon = "audio-volume-high"; - break; - case VolumeControl.ActiveOutput.USB_HEADPHONES: - icon = "audio-volume-high"; - break; - case VolumeControl.ActiveOutput.HDMI_SPEAKER: - icon = "audio-volume-high"; - break; - case VolumeControl.ActiveOutput.HDMI_HEADPHONES: - icon = "audio-volume-high"; - break; - } - } else { - icon = get_volume_icon (volume, active_output); - } - return icon; - } - - private string get_volume_root_icon (double volume, bool mute, VolumeControl.ActiveOutput active_output) { - string icon = ""; - switch (active_output) - { - case VolumeControl.ActiveOutput.SPEAKERS: - if (mute || volume <= 0.0) - icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; - else if (this.accounts_service != null && this.accounts_service.silentMode) - icon = "audio-volume-muted-panel"; - else - icon = get_volume_root_icon_by_volume (volume, active_output); - break; - case VolumeControl.ActiveOutput.HEADPHONES: - if (mute || volume <= 0.0) - icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; - else if (this.accounts_service != null && this.accounts_service.silentMode) - icon = "audio-volume-muted-panel"; - else - icon = get_volume_root_icon_by_volume (volume, active_output); - break; - case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: if (mute || volume <= 0.0) - icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; - else if (this.accounts_service != null && this.accounts_service.silentMode) - icon = "audio-volume-muted-panel"; - else - icon = get_volume_root_icon_by_volume (volume, active_output); - break; - case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: - if (mute || volume <= 0.0) - icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; - else if (this.accounts_service != null && this.accounts_service.silentMode) - icon = "audio-volume-muted-panel"; - else - icon = get_volume_root_icon_by_volume (volume, active_output); - break; - case VolumeControl.ActiveOutput.USB_SPEAKER: - if (mute || volume <= 0.0) - icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; - else if (this.accounts_service != null && this.accounts_service.silentMode) - icon = "audio-volume-muted-panel"; - else - icon = get_volume_root_icon_by_volume (volume, active_output); - break; - case VolumeControl.ActiveOutput.USB_HEADPHONES: - if (mute || volume <= 0.0) - icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; - else if (this.accounts_service != null && this.accounts_service.silentMode) - icon = "audio-volume-muted-panel"; - else - icon = get_volume_root_icon_by_volume (volume, active_output); - break; - case VolumeControl.ActiveOutput.HDMI_SPEAKER: - if (mute || volume <= 0.0) - icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; - else if (this.accounts_service != null && this.accounts_service.silentMode) - icon = "audio-volume-muted-panel"; - else - icon = get_volume_root_icon_by_volume (volume, active_output); - break; - case VolumeControl.ActiveOutput.HDMI_HEADPHONES: - if (mute || volume <= 0.0) - icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; - else if (this.accounts_service != null && this.accounts_service.silentMode) - icon = "audio-volume-muted-panel"; - else - icon = get_volume_root_icon_by_volume (volume, active_output); - break; - } - return icon; - } + return this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; + if (this.accounts_service != null && this.accounts_service.silentMode) + return "audio-volume-muted-panel"; + return get_volume_root_icon_by_volume (volume, active_output); - private string get_notification_label () { - string volume_label = ""; - switch (volume_control.active_output) - { - case VolumeControl.ActiveOutput.SPEAKERS: - volume_label = _("Speakers"); - break; - case VolumeControl.ActiveOutput.HEADPHONES: - volume_label = _("Headphones"); - break; - case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: - volume_label = _("Bluetooth headphones"); - break; - case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: - volume_label = _("Bluetooth speaker"); - break; - case VolumeControl.ActiveOutput.USB_SPEAKER: - volume_label = _("Usb speaker"); - break; - case VolumeControl.ActiveOutput.USB_HEADPHONES: - volume_label = _("Usb headphones"); - break; - case VolumeControl.ActiveOutput.HDMI_SPEAKER: - volume_label = _("HDMI speaker"); - break; - case VolumeControl.ActiveOutput.HDMI_HEADPHONES: - volume_label = _("HDMI headphones"); - break; + default: + return ""; } - - return volume_label; } private void update_notification () { - - List<string> caps = Notify.get_server_caps (); - 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; - - var loud = volume_control.high_volume; - bool ignore_warning_this_time = this.volume_control.ignore_high_volume; - var warn = loud - && this.notify_server_supports_actions - && !this.volume_control.high_volume_approved - && !ignore_warning_this_time; - if (waiting_user_approve_warn && volume_control.below_warning_volume) { - volume_control.set_warning_volume(); - close_notification(warn_notification); - } - if (warn) { - close_notification(info_notification); - if (_pre_warn_volume == null) { - _pre_warn_volume = new VolumeControl.Volume(); - _pre_warn_volume.volume = volume_control.volume.volume; - _pre_warn_volume.reason = volume_control.volume.reason; - } - warn_notification.clear_actions(); - warn_notification.add_action ("ok", _("OK"), (n, a) => { - stop_clamp_to_high_timeout(); - volume_control.approve_high_volume (); - // restore the volume the user introduced - VolumeControl.Volume vol = new VolumeControl.Volume(); - vol.volume = volume_control.get_pre_clamped_volume(); - vol.reason = VolumeControl.VolumeReasons.USER_KEYPRESS; - _pre_warn_volume = null; - volume_control.volume = vol; - - waiting_user_approve_warn = false; - }); - warn_notification.add_action ("cancel", _("Cancel"), (n, a) => { - _pre_warn_volume = null; - waiting_user_approve_warn = false; - increment_volume_sync_action(); - }); - waiting_user_approve_warn = true; - show_notification(warn_notification); - } else { - if (!waiting_user_approve_warn) { - close_notification(warn_notification); - - if (notify_server_supports_sync && !block_info_notifications && !ignore_warning_this_time) { - /* Determine Label */ - string volume_label = get_notification_label (); - - /* Choose an icon */ - string icon = get_volume_notification_icon (volume_control.volume.volume, loud, volume_control.active_output); - - /* 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(get_volume_percent() * 100.0)); - show_notification(n); - } - } + if (!_volume_warning.active && !block_info_notifications) { + _info_notification.show(this.volume_control.active_output(), + get_volume_percent(), + _volume_warning.high_volume); } } @@ -738,7 +394,7 @@ public class IndicatorSound.Service: Object { this.mute_blocks_sound = false; this.sound_was_blocked_timeout_id = 0; this.update_root_icon (); - return false; + return Source.REMOVE; }); } @@ -750,10 +406,10 @@ public class IndicatorSound.Service: Object { /* return the current volume in the range of [0.0, 1.0] */ private double get_volume_percent() { - return volume_control.volume.volume / this.volume_control.max_volume; + return volume_control.volume.volume / _options.max_volume; } - /* volume control's range can vary depending on its max_volume property, + /* volume control's range can vary depending on options.max_volume, * but the action always needs to be in [0.0, 1.0]... */ private Variant create_volume_action_state() { return new Variant.double (get_volume_percent()); @@ -768,14 +424,14 @@ public class IndicatorSound.Service: Object { volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, create_volume_action_state()); volume_action.change_state.connect ( (action, val) => { - double v = val.get_double () * this.volume_control.max_volume; + double v = val.get_double () * _options.max_volume; volume_control.set_volume_clamp (v, VolumeControl.VolumeReasons.USER_KEYPRESS); }); /* activating this action changes the volume by the amount given in the parameter */ volume_action.activate.connect ((a,p) => activate_scroll_action(a,p)); - this.volume_control.notify["max-volume"].connect(() => { + _options.notify["max-volume"].connect(() => { update_volume_action_state(); }); @@ -788,9 +444,6 @@ public class IndicatorSound.Service: Object { if (reason == VolumeControl.VolumeReasons.USER_KEYPRESS || reason == VolumeControl.VolumeReasons.DEVICE_OUTPUT_CHANGE) this.update_notification (); - - if ((warn_notification.id != 0) && (_pre_warn_volume != null)) - clamp_to_high_soon(); }); this.volume_control.bind_property ("ready", volume_action, "enabled", BindingFlags.SYNC_CREATE); @@ -815,12 +468,19 @@ public class IndicatorSound.Service: Object { return mic_volume_action; } + private Variant create_high_volume_action_state() { + return new Variant.boolean (_volume_warning.high_volume); + } + private void update_high_volume_action_state() { + high_volume_action.set_state(create_high_volume_action_state()); + } + SimpleAction high_volume_action; Action create_high_volume_action () { - high_volume_action = new SimpleAction.stateful("high-volume", null, new Variant.boolean (this.volume_control.high_volume)); + high_volume_action = new SimpleAction.stateful("high-volume", null, create_high_volume_action_state()); - this.volume_control.notify["high-volume"].connect( () => { - high_volume_action.set_state(new Variant.boolean (this.volume_control.high_volume)); + _volume_warning.notify["high-volume"].connect( () => { + update_high_volume_action_state(); update_notification(); }); @@ -842,7 +502,7 @@ public class IndicatorSound.Service: Object { uint export_actions = 0; Variant action_state_for_player (MediaPlayer player, bool show_track = true) { - var builder = new VariantBuilder (new VariantType ("a{sv}")); + var builder = new VariantBuilder (VariantType.VARDICT); builder.add ("{sv}", "running", new Variant ("b", player.is_running)); builder.add ("{sv}", "state", new Variant ("s", player.state)); if (player.current_track != null && show_track) { @@ -881,7 +541,7 @@ public class IndicatorSound.Service: Object { clear_acts_player(); this.player_action_update_id = 0; - return false; + return Source.REMOVE; } void eventually_update_player_actions () { @@ -956,27 +616,4 @@ public class IndicatorSound.Service: Object { this.update_preferred_players (); } - - /** VOLUME CLAMPING **/ - - private uint _clamp_to_high_timeout = 0; - - private void stop_clamp_to_high_timeout() { - if (_clamp_to_high_timeout != 0) { - Source.remove(_clamp_to_high_timeout); - _clamp_to_high_timeout = 0; - } - } - - private void clamp_to_high_soon() { - const uint interval_msec = 200; - if (_clamp_to_high_timeout == 0) - _clamp_to_high_timeout = Timeout.add(interval_msec, clamp_to_high_idle); - } - - private bool clamp_to_high_idle() { - _clamp_to_high_timeout = 0; - volume_control.clamp_to_high_volume(); - return false; // Source.REMOVE; - } } diff --git a/src/volume-control-pulse.vala b/src/volume-control-pulse.vala index 4bd3076..6021447 100644 --- a/src/volume-control-pulse.vala +++ b/src/volume-control-pulse.vala @@ -22,9 +22,6 @@ using PulseAudio; using Notify; using Gee; -[CCode(cname="pa_cvolume_set", cheader_filename = "pulse/volume.h")] -extern unowned PulseAudio.CVolume? vol_set (PulseAudio.CVolume? cv, uint channels, PulseAudio.Volume v); - [DBus (name="com.canonical.UnityGreeter.List")] interface GreeterListInterface : Object { @@ -34,19 +31,14 @@ interface GreeterListInterface : Object public class VolumeControlPulse : VolumeControl { - /* this is static to ensure it being freed after @context (loop does not have ref counting) */ - private static PulseAudio.GLibMainLoop loop; + private unowned PulseAudio.GLibMainLoop loop = null; private uint _reconnect_timer = 0; private PulseAudio.Context context; private bool _mute = true; - private bool _is_playing = false; - private bool _ignore_warning_this_time = false; private VolumeControl.Volume _volume = new VolumeControl.Volume(); private double _mic_volume = 0.0; - private Settings _settings = new Settings ("com.canonical.indicator.sound"); - private Settings _shared_settings = new Settings ("com.ubuntu.sound"); /* Used by the pulseaudio stream restore extension */ private DBusConnection _pconn; @@ -57,22 +49,6 @@ public class VolumeControlPulse : VolumeControl private bool _pulse_use_stream_restore = false; private int32 _active_sink_input = -1; private string[] _valid_roles = {"multimedia", "alert", "alarm", "phone"}; - public override string stream { - get { - if (_active_sink_input == -1) - return "alert"; - var path = _sink_input_hash[_active_sink_input]; - if (path == _objp_role_multimedia) - return "multimedia"; - if (path == _objp_role_alert) - return "alert"; - if (path == _objp_role_alarm) - return "alarm"; - if (path == _objp_role_phone) - return "phone"; - return "alert"; - } - } private string? _objp_role_multimedia = null; private string? _objp_role_alert = null; private string? _objp_role_alarm = null; @@ -87,40 +63,28 @@ public class VolumeControlPulse : VolumeControl private uint _accountservice_volume_timer = 0; private bool _send_next_local_volume = false; private double _account_service_volume = 0.0; - private bool _active_port_headphone = false; private VolumeControl.ActiveOutput _active_output = VolumeControl.ActiveOutput.SPEAKERS; - /** true when connected to the pulse server */ - public override bool ready { get; private set; } - /** true when a microphone is active **/ public override bool active_mic { get; private set; default = false; } - public VolumeControlPulse () + public VolumeControlPulse (IndicatorSound.Options options, PulseAudio.GLibMainLoop loop) { + base(options); + _volume.volume = 0.0; _volume.reason = VolumeControl.VolumeReasons.PULSE_CHANGE; - if (loop == null) - loop = new PulseAudio.GLibMainLoop (); + this.loop = loop; _mute_cancellable = new Cancellable (); _volume_cancellable = new Cancellable (); - init_all_properties(); - setup_accountsservice.begin (); this.reconnect_to_pulse (); } - private void init_all_properties() - { - init_max_volume(); - init_high_volume(); - init_high_volume_approved(); - } - ~VolumeControlPulse () { stop_all_timers(); @@ -134,10 +98,9 @@ public class VolumeControlPulse : VolumeControl } stop_local_volume_timer(); stop_account_service_volume_timer(); - stop_high_volume_approved_timer(); } - private VolumeControl.ActiveOutput calculate_active_output (SinkInfo? sink) { + 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 */ @@ -155,9 +118,8 @@ public class VolumeControlPulse : VolumeControl (sink.active_port != null && (sink.active_port.name.contains("headset") || sink.active_port.name.contains("headphone")))) { - _active_port_headphone = true; // check if it's a bluetooth device - var device_bus = sink.proplist.gets ("device.bus"); + 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") { @@ -169,8 +131,7 @@ public class VolumeControlPulse : VolumeControl } } else { // speaker - _active_port_headphone = false; - var device_bus = sink.proplist.gets ("device.bus"); + 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") { @@ -245,28 +206,19 @@ public class VolumeControlPulse : VolumeControl } var playing = (i.state == PulseAudio.SinkState.RUNNING); - if (_is_playing != playing) - { - _is_playing = playing; - this.notify_property ("is-playing"); - } + if (is_playing != playing) + is_playing = playing; - // store the current status of the active output - VolumeControl.ActiveOutput active_output_before = active_output; + var oldval = _active_output; + var newval = calculate_active_output(i); - // calculate the output - _active_output = calculate_active_output (i); - - // check if the output has changed, if so... emit a signal - VolumeControl.ActiveOutput active_output_now = active_output; - if (active_output_now != active_output_before && - (active_output_now != VolumeControl.ActiveOutput.CALL_MODE && - active_output_before != VolumeControl.ActiveOutput.CALL_MODE)) { - this.active_output_changed (active_output_now); - if (active_output_now == VolumeControl.ActiveOutput.SPEAKERS) { - _high_volume_approved = false; - } - update_high_volume(); + _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 && @@ -345,10 +297,6 @@ public class VolumeControlPulse : VolumeControl var vol = new VolumeControl.Volume(); vol.volume = volume_to_double (lvolume); vol.reason = VolumeControl.VolumeReasons.PULSE_CHANGE; - // Ignore changes from PULSE to avoid issues with - // some apps that change the volume in the sink - // We only take into account volume changes from the user - this._ignore_warning_this_time = true; this.volume = vol; } } @@ -358,6 +306,21 @@ public class VolumeControlPulse : VolumeControl 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)) { @@ -365,10 +328,14 @@ public class VolumeControlPulse : VolumeControl 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 (new VariantType ("ao")); + var builder = new VariantBuilder (VariantType.OBJECT_PATH_ARRAY); builder.add ("o", sink_input_objp); yield _pconn.call ("org.PulseAudio.Core1", "/org/pulseaudio/core1", @@ -393,10 +360,6 @@ public class VolumeControlPulse : VolumeControl var vol = new VolumeControl.Volume(); vol.volume = volume_to_double (volume); vol.reason = VolumeControl.VolumeReasons.VOLUME_STREAM_CHANGE; - // Ignore changes from PULSE to avoid issues with - // some apps that change the volume in the sink - // We only take into account volume changes from the user - this._ignore_warning_this_time = true; this.volume = vol; } catch (GLib.Error e) { warning ("unable to get volume for active role %s (%s)", sink_input_objp, e.message); @@ -407,7 +370,7 @@ public class VolumeControlPulse : VolumeControl private void add_sink_input_into_list (SinkInputInfo sink_input) { /* We're only adding ones that are not corked and with a valid role */ - var role = sink_input.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_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") { @@ -477,7 +440,7 @@ public class VolumeControlPulse : VolumeControl if (i == null) return; - var role = i.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE); + unowned string role = i.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE); if (role == "phone" || role == "production") this.active_mic = true; } @@ -499,7 +462,7 @@ public class VolumeControlPulse : VolumeControl c.set_subscribe_callback (context_events_cb); update_sink (); update_source (); - this.ready = true; + this.ready = true; // true because we're connected to the pulse server break; case Context.State.FAILED: @@ -518,7 +481,7 @@ public class VolumeControlPulse : VolumeControl { _reconnect_timer = 0; reconnect_to_pulse (); - return false; // G_SOURCE_REMOVE + return Source.REMOVE; } void reconnect_to_pulse () @@ -540,7 +503,7 @@ public class VolumeControlPulse : VolumeControl this.context = new PulseAudio.Context (loop.get_api(), null, props); this.context.set_state_callback (context_state_callback); - var server_string = Environment.get_variable("PULSE_SERVER"); + 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())); } @@ -590,30 +553,19 @@ public class VolumeControlPulse : VolumeControl } } - public override bool is_playing + public override VolumeControl.ActiveOutput active_output() { - get - { - return this._is_playing; - } - } - - public override VolumeControl.ActiveOutput active_output - { - get - { - return _active_output; - } + return _active_output; } /* Volume operations */ - private static PulseAudio.Volume double_to_volume (double vol) + 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; } - private static double volume_to_double (PulseAudio.Volume vol) + 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); @@ -668,8 +620,6 @@ public class VolumeControlPulse : VolumeControl active_role_objp, "org.freedesktop.DBus.Properties", "Set", new Variant ("(ssv)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume", volume), null, DBusCallFlags.NONE, -1); - - debug ("Set volume to %f on path %s", vol, active_role_objp); } catch (GLib.Error e) { lock (_pa_volume_sig_count) { _pa_volume_sig_count--; @@ -687,7 +637,7 @@ public class VolumeControlPulse : VolumeControl void set_mic_volume_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) { if (i != null) { unowned CVolume cvol = CVolume (); - cvol = vol_set (cvol, 1, double_to_volume (_mic_volume)); + cvol.set (1, double_to_volume (_mic_volume)); c.set_source_volume_by_name (i.default_source_name, cvol, set_mic_volume_success_cb); } } @@ -716,170 +666,9 @@ public class VolumeControlPulse : VolumeControl && volume_changed) { start_local_volume_timer(); } - - update_high_volume(); } } - /** MAX VOLUME PROPERTY **/ - - private void init_max_volume() { - _settings.changed["normal-volume-decibels"].connect(() => update_max_volume()); - _settings.changed["amplified-volume-decibels"].connect(() => update_max_volume()); - _shared_settings.changed["allow-amplified-volume"].connect(() => update_max_volume()); - update_max_volume(); - } - private void update_max_volume () { - var new_max_volume = calculate_max_volume(); - if (max_volume != new_max_volume) { - debug("changing max_volume from %f to %f", this.max_volume, new_max_volume); - max_volume = calculate_max_volume(); - } - } - private double calculate_max_volume () { - unowned string decibel_key = _shared_settings.get_boolean("allow-amplified-volume") - ? "amplified-volume-decibels" - : "normal-volume-decibels"; - var volume_dB = _settings.get_double(decibel_key); - var volume_sw = PulseAudio.Volume.sw_from_dB (volume_dB); - return volume_to_double (volume_sw); - } - - /** HIGH VOLUME PROPERTY **/ - - private bool _warning_volume_enabled; - private double _warning_volume_norms; /* 1.0 == PA_VOLUME_NORM */ - private bool _high_volume = false; - public override bool ignore_high_volume { - get { - if (_ignore_warning_this_time) { - warning("Ignore"); - _ignore_warning_this_time = false; - return true; - } - return false; - } - set { } - } - public override bool high_volume { - get { return this._high_volume; } - private set { this._high_volume = value; } - } - public override bool below_warning_volume { - get { return this._volume.volume < this._warning_volume_norms; } - private set { } - } - private void init_high_volume() { - _settings.changed["warning-volume-enabled"].connect(() => update_high_volume_cache()); - _settings.changed["warning-volume-decibels"].connect(() => update_high_volume_cache()); - update_high_volume_cache(); - } - private void update_high_volume_cache() { - var volume_dB = _settings.get_double ("warning-volume-decibels"); - var volume_sw = PulseAudio.Volume.sw_from_dB (volume_dB); - var volume_norms = volume_to_double (volume_sw); - _warning_volume_norms = volume_norms; - _warning_volume_enabled = _settings.get_boolean("warning-volume-enabled"); - debug("updating high volume cache... enabled %d dB %f sw %lu norm %f", (int)_warning_volume_enabled, volume_dB, volume_sw, volume_norms); - update_high_volume(); - } - private void update_high_volume() { - var new_high_volume = calculate_high_volume(); - if (high_volume != new_high_volume) { - debug("changing high_volume from %d to %d", (int)high_volume, (int)new_high_volume); - high_volume = new_high_volume; - } - } - private bool calculate_high_volume() { - return calculate_high_volume_from_volume(_volume.volume); - } - private bool calculate_high_volume_from_volume(double volume) { - return _active_port_headphone - && _warning_volume_enabled - && volume > _warning_volume_norms - && (stream == "multimedia"); - } - - public override void clamp_to_high_volume() { - if (_high_volume && (_volume.volume > _warning_volume_norms)) { - var vol = new VolumeControl.Volume(); - vol.volume = _volume.volume.clamp(0, _warning_volume_norms); - vol.reason = _volume.reason; - debug("Clamping from %f down to %f", _volume.volume, vol.volume); - volume = vol; - } - } - - public override void set_warning_volume() { - var vol = new VolumeControl.Volume(); - vol.volume = _warning_volume_norms; - vol.reason = _volume.reason; - debug("Setting warning level volume from %f down to %f", _volume.volume, vol.volume); - volume = vol; - } - - /** HIGH VOLUME APPROVED PROPERTY **/ - - private bool _high_volume_approved = false; - private uint _high_volume_approved_timer = 0; - private int64 _high_volume_approved_at = 0; - private int64 _high_volume_approved_ttl_usec = 0; - public override bool high_volume_approved { - get { return this._high_volume_approved; } - private set { this._high_volume_approved = value; } - } - private void init_high_volume_approved() { - _settings.changed["warning-volume-confirmation-ttl"].connect(() => update_high_volume_approved_cache()); - update_high_volume_approved_cache(); - } - private void update_high_volume_approved_cache() { - _high_volume_approved_ttl_usec = _settings.get_int("warning-volume-confirmation-ttl"); - _high_volume_approved_ttl_usec *= 1000000; - - update_high_volume_approved(); - update_high_volume_approved_timer(); - } - private void update_high_volume_approved_timer() { - stop_high_volume_approved_timer(); - if (_high_volume_approved_at != 0) { - int64 expires_at = _high_volume_approved_at + _high_volume_approved_ttl_usec; - int64 now = GLib.get_monotonic_time(); - if (expires_at > now) { - var seconds_left = 1 + ((expires_at - now) / 1000000); - _high_volume_approved_timer = Timeout.add_seconds((uint)seconds_left, on_high_volume_approved_timer); - } - } - } - private void stop_high_volume_approved_timer() { - if (_high_volume_approved_timer != 0) { - Source.remove (_high_volume_approved_timer); - _high_volume_approved_timer = 0; - } - } - private bool on_high_volume_approved_timer() { - _high_volume_approved_timer = 0; - update_high_volume_approved(); - return false; /* Source.REMOVE */ - } - private void update_high_volume_approved() { - var new_high_volume_approved = calculate_high_volume_approved(); - if (high_volume_approved != new_high_volume_approved) { - debug("changing high_volume_approved from %d to %d", (int)high_volume_approved, (int)new_high_volume_approved); - high_volume_approved = new_high_volume_approved; - } - } - private bool calculate_high_volume_approved() { - int64 now = GLib.get_monotonic_time(); - return (_high_volume_approved_at != 0) - && (_high_volume_approved_at + _high_volume_approved_ttl_usec >= now); - } - public override void approve_high_volume() { - _high_volume_approved_at = GLib.get_monotonic_time(); - update_high_volume_approved(); - update_high_volume_approved_timer(); - } - - /** MIC VOLUME PROPERTY */ public override double mic_volume { @@ -895,16 +684,11 @@ public class VolumeControlPulse : VolumeControl } } - /* PulseAudio Dbus (Stream Restore) logic */ - private void reconnect_pulse_dbus () + public static DBusConnection? create_pulse_dbus_connection() { unowned string pulse_dbus_server_env = Environment.get_variable ("PULSE_DBUS_SERVER"); string address; - /* In case of a reconnect */ - _pulse_use_stream_restore = false; - _pa_volume_sig_count = 0; - if (pulse_dbus_server_env != null) { address = pulse_dbus_server_env; } else { @@ -915,7 +699,7 @@ public class VolumeControlPulse : VolumeControl conn = Bus.get_sync (BusType.SESSION); } catch (GLib.IOError e) { warning ("unable to get the dbus session bus: %s", e.message); - return; + return null; } try { @@ -927,27 +711,41 @@ public class VolumeControlPulse : VolumeControl address = props.get_string (); } catch (GLib.Error e) { warning ("unable to get pulse unix socket: %s", e.message); - return; + return null; } } - debug ("PulseAudio dbus unix socket: %s", address); + DBusConnection conn = null; try { - _pconn = new DBusConnection.for_address_sync (address, DBusConnectionFlags.AUTHENTICATION_CLIENT); + 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 */ - return; } + 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 ("sink-input-by-media-role:multimedia"); - _objp_role_alert = stream_restore_get_object_path ("sink-input-by-media-role:alert"); - _objp_role_alarm = stream_restore_get_object_path ("sink-input-by-media-role:alarm"); - _objp_role_phone = stream_restore_get_object_path ("sink-input-by-media-role: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) { @@ -958,10 +756,10 @@ public class VolumeControlPulse : VolumeControl } } - private string? stream_restore_get_object_path (string name) { + 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", + 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 */ @@ -977,7 +775,7 @@ public class VolumeControlPulse : VolumeControl /* AccountsService operations */ private void accountsservice_props_changed_cb (DBusProxy proxy, Variant changed_properties, string[]? invalidated_properties) { - Variant volume_variant = changed_properties.lookup_value ("Volume", new VariantType ("d")); + Variant volume_variant = changed_properties.lookup_value ("Volume", VariantType.DOUBLE); if (volume_variant != null) { var volume = volume_variant.get_double (); if (volume >= 0) { @@ -987,7 +785,7 @@ public class VolumeControlPulse : VolumeControl } } - Variant mute_variant = changed_properties.lookup_value ("Muted", new VariantType ("b")); + Variant mute_variant = changed_properties.lookup_value ("Muted", VariantType.BOOLEAN); if (mute_variant != null) { var mute = mute_variant.get_boolean (); set_mute_internal (mute); @@ -1061,7 +859,7 @@ public class VolumeControlPulse : VolumeControl yield setup_user_proxy (); } else { // We are in a user session. We just need our own proxy - var username = Environment.get_variable ("USER"); + unowned string username = Environment.get_variable ("USER"); if (username != "" && username != null) { yield setup_user_proxy (username); } @@ -1128,7 +926,7 @@ public class VolumeControlPulse : VolumeControl _send_next_local_volume = false; start_local_volume_timer (); } - return false; // G_SOURCE_REMOVE + return Source.REMOVE; } private void start_account_service_volume_timer() @@ -1160,6 +958,6 @@ public class VolumeControlPulse : VolumeControl { _accountservice_volume_timer = 0; start_account_service_volume_timer (); - return false; // G_SOURCE_REMOVE + return Source.REMOVE; } } diff --git a/src/volume-control.vala b/src/volume-control.vala index 90fc325..3d02f70 100644 --- a/src/volume-control.vala +++ b/src/volume-control.vala @@ -40,36 +40,39 @@ public abstract class VolumeControl : Object CALL_MODE } + public enum Stream { + ALERT, + MULTIMEDIA, + ALARM, + PHONE + } + public class Volume : Object { public double volume; public VolumeReasons reason; } - public virtual string stream { get { return ""; } } - public virtual bool ready { get { return false; } set { } } + protected IndicatorSound.Options _options = null; + + public VolumeControl(IndicatorSound.Options options) { + _options = options; + } + + public Stream active_stream { get; protected set; default = Stream.ALERT; } + public bool ready { get; protected set; default = false; } public virtual bool active_mic { get { return false; } set { } } - public virtual bool high_volume { get { return false; } protected set { } } - public virtual bool ignore_high_volume { get { return false; } protected set { } } - public virtual bool below_warning_volume { get { return false; } protected set { } } public virtual bool mute { get { return false; } } - public virtual bool is_playing { get { return false; } } - public virtual VolumeControl.ActiveOutput active_output { get { return VolumeControl.ActiveOutput.SPEAKERS; } } + public bool is_playing { get; protected set; default = false; } private Volume _volume; private double _pre_clamp_volume; public virtual Volume volume { get { return _volume; } set { } } public virtual double mic_volume { get { return 0.0; } set { } } - public virtual double max_volume { get { return 1.0; } protected set { } } - - public virtual bool high_volume_approved { get { return false; } protected set { } } - public virtual void approve_high_volume() { } - public virtual void clamp_to_high_volume() { } - public virtual void set_warning_volume() { } public abstract void set_mute (bool mute); public void set_volume_clamp (double unclamped, VolumeControl.VolumeReasons reason) { var v = new VolumeControl.Volume(); - v.volume = unclamped.clamp (0.0, this.max_volume); + v.volume = unclamped.clamp (0.0, _options.max_volume); v.reason = reason; this.volume = v; _pre_clamp_volume = unclamped; @@ -79,5 +82,6 @@ public abstract class VolumeControl : Object return _pre_clamp_volume; } + public abstract VolumeControl.ActiveOutput active_output(); public signal void active_output_changed (VolumeControl.ActiveOutput active_output); } diff --git a/src/volume-warning-pulse.vala b/src/volume-warning-pulse.vala new file mode 100644 index 0000000..2492cef --- /dev/null +++ b/src/volume-warning-pulse.vala @@ -0,0 +1,211 @@ +/* + * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*- + * + * Copyright 2015 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +using PulseAudio; + +/** + * A VolumeWarning that uses PulseAudio to + * (a) implement sound_system_set_multimedia_volume() and + * (b) keep the multimedia_active and multimedia_volume properties up-to-date + */ +public class VolumeWarningPulse : VolumeWarning +{ + public VolumeWarningPulse (IndicatorSound.Options options, + PulseAudio.GLibMainLoop pgloop) { + base (options); + + _pgloop = pgloop; + pulse_reconnect (); + } + + ~VolumeWarningPulse () { + clear_timer (ref _pulse_reconnect_timer); + clear_timer (ref _pending_sink_inputs_timer); + pulse_disconnect (); + } + + protected override void preshow () { + /* showing the dialog can change the sink input index (bug #1484589) + * so cache it here for later use in sound_system_set_multimedia_volume() */ + _target_sink_input_index = _multimedia_sink_input_index; + } + + protected override void sound_system_set_multimedia_volume (PulseAudio.Volume volume) { + var index = _target_sink_input_index; + + return_if_fail (_pulse_context != null); + return_if_fail (index != PulseAudio.INVALID_INDEX); + return_if_fail (volume != PulseAudio.Volume.INVALID); + + unowned CVolume cvol = CVolume (); + cvol.set (1, volume); + debug ("setting multimedia (sink_input index %d) volume to %s", (int)index, cvol.to_string ()); + _pulse_context.set_sink_input_volume (index, cvol); + } + + private unowned PulseAudio.GLibMainLoop _pgloop = null; + private PulseAudio.Context _pulse_context = null; + private uint _pulse_reconnect_timer = 0; + private uint _pending_sink_inputs_timer = 0; + private GenericSet<uint32> _pending_sink_inputs = new GenericSet<uint32>(direct_hash, direct_equal); + + private uint soon_interval_msec = 500; + + private uint32 _target_sink_input_index = PulseAudio.INVALID_INDEX; + private uint32 _multimedia_sink_input_index = PulseAudio.INVALID_INDEX; + + /***/ + + private bool is_active_multimedia (SinkInputInfo i) { + return (i.corked == 0) && + (i.proplist.gets(PulseAudio.Proplist.PROP_MEDIA_ROLE) == "multimedia"); + + } + + private void clear_multimedia () { + _multimedia_sink_input_index = PulseAudio.INVALID_INDEX; + multimedia_volume = PulseAudio.Volume.INVALID; + multimedia_active = false; + } + + private void on_sink_input_info (Context c, SinkInputInfo? i, int eol) { + + if (i == null) + return; + + if (is_active_multimedia (i)) { + GLib.debug ("on_sink_input_info() setting multimedia sink input index to %d, sink index to %d", (int)i.index, (int)i.sink); + _multimedia_sink_input_index = i.index; + multimedia_volume = i.volume.max (); + multimedia_active = true; + } + else if (i.index == _multimedia_sink_input_index) { + clear_multimedia (); + } + } + + private void update_all_sink_inputs () { + _pulse_context.get_sink_input_info_list (on_sink_input_info); + } + private void update_sink_input (uint32 index) { + _pulse_context.get_sink_input_info (index, on_sink_input_info); + } + + private void update_sink_input_soon (uint32 index) { + + _pending_sink_inputs.add (index); + + if (_pending_sink_inputs_timer == 0) { + _pending_sink_inputs_timer = Timeout.add (soon_interval_msec, () => { + _pending_sink_inputs_timer = 0; + _pending_sink_inputs.foreach ((i) => update_sink_input (i)); + _pending_sink_inputs.remove_all (); + return Source.REMOVE; + }); + } + } + + private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index) { + switch (t & Context.SubscriptionEventType.FACILITY_MASK) + { + case Context.SubscriptionEventType.SINK_INPUT: + switch (t & Context.SubscriptionEventType.TYPE_MASK) + { + // if a SinkInput changed, get its updated info + // to keep our multimedia indices up-to-date + case Context.SubscriptionEventType.NEW: + case Context.SubscriptionEventType.CHANGE: + update_sink_input_soon (index); + break; + + // if the multimedia sink input was removed, + // reset our mm fields and look for a new mm sink input + case Context.SubscriptionEventType.REMOVE: + if (index == _multimedia_sink_input_index) { + clear_multimedia (); + update_all_sink_inputs (); + } + break; + + default: + GLib.debug ("Sink input event not known."); + break; + } + break; + + default: + break; + } + } + + private void pulse_context_state_callback (Context c) { + switch (c.get_state ()) { + case Context.State.READY: + c.set_subscribe_callback (context_events_cb); + c.subscribe (PulseAudio.Context.SubscriptionMask.SINK | + PulseAudio.Context.SubscriptionMask.SINK_INPUT); + update_all_sink_inputs (); + break; + + case Context.State.FAILED: + case Context.State.TERMINATED: + pulse_reconnect_soon (); + break; + + default: + break; + } + } + + private void pulse_disconnect () { + if (_pulse_context != null) { + _pulse_context.disconnect (); + _pulse_context = null; + } + } + + private void pulse_reconnect_soon () { + if (_pulse_reconnect_timer == 0) { + _pulse_reconnect_timer = Timeout.add_seconds (2, () => { + _pulse_reconnect_timer = 0; + pulse_reconnect (); + return Source.REMOVE; + }); + } + } + + void pulse_reconnect () { + pulse_disconnect (); + + 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"); + + _pulse_context = new PulseAudio.Context (_pgloop.get_api(), null, props); + _pulse_context.set_state_callback (pulse_context_state_callback); + + unowned string server_string = Environment.get_variable ("PULSE_SERVER"); + if (_pulse_context.connect (server_string, Context.Flags.NOFAIL, null) < 0) + GLib.warning ("pa_context_connect() failed: %s\n", PulseAudio.strerror(_pulse_context.errno())); + } +} diff --git a/src/volume-warning.vala b/src/volume-warning.vala new file mode 100644 index 0000000..3c0f1e6 --- /dev/null +++ b/src/volume-warning.vala @@ -0,0 +1,216 @@ +/* + * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*- + * + * Copyright 2015 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +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); + } + } + + public 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) + && (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 ("com.canonical.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 under the warning level + sound_system_set_multimedia_volume (_options.loud_volume-1); + } + + 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); + } + + _ok_volume = PulseAudio.Volume.INVALID; + } +} diff --git a/src/warn-notification.vala b/src/warn-notification.vala new file mode 100644 index 0000000..17129aa --- /dev/null +++ b/src/warn-notification.vala @@ -0,0 +1,59 @@ +/* + * Copyright 2015 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +public class IndicatorSound.WarnNotification: Notification +{ + public enum Response { + CANCEL, + OK + } + + public signal void user_responded (Response response); + + protected override Notify.Notification create_notification () { + var n = new Notify.Notification ( + _("Volume"), + _("High volume can damage your hearing."), + "audio-volume-high"); + n.set_hint ("x-canonical-non-shaped-icon", "true"); + n.set_hint ("x-canonical-snap-decisions", "true"); + n.set_hint ("x-canonical-private-affirmative-tint", "true"); + n.closed.connect ((n) => { + n.clear_actions (); + }); + return n; + } + + public bool show () { + + if (!notify_server_supports ("actions")) + return false; + + _notification.clear_actions (); + _notification.add_action ("ok", _("OK"), (n, a) => { + user_responded (Response.OK); + }); + _notification.add_action ("cancel", _("Cancel"), (n, a) => { + user_responded (Response.CANCEL); + }); + show_notification(); + + return true; + } +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bf0e051..e69c01e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -72,9 +72,17 @@ vala_add(vala-mocks ) vala_add(vala-mocks + options-mock.vala +) + +vala_add(vala-mocks volume-control-mock.vala ) +vala_add(vala-mocks + volume-warning-mock.vala +) + vala_finish(vala-mocks SOURCES vala_mocks_VALA_SOURCES @@ -169,6 +177,7 @@ add_executable (volume-control-test volume-control-test.cc gschemas.compiled) target_link_libraries ( volume-control-test indicator-sound-service-lib + vala-mocks-lib pulse-mock gtest-static ${TEST_LIBRARIES} @@ -209,7 +218,7 @@ target_link_libraries ( ${TEST_LIBRARIES} ) -#add_test(notifications-test notifications-test) +add_test(notifications-test notifications-test) ########################### # Accounts Service User diff --git a/tests/integration/indicator-sound-test-base.cpp b/tests/integration/indicator-sound-test-base.cpp index 3b9c58d..91abf42 100644 --- a/tests/integration/indicator-sound-test-base.cpp +++ b/tests/integration/indicator-sound-test-base.cpp @@ -163,13 +163,31 @@ bool IndicatorSoundTestBase::clearGSettingsPlayers() << "com.canonical.indicator.sound" << "interested-media-players" << "[]"); - if (!clearPlayers.waitForStarted()) + + return runProcess(clearPlayers); +} + +bool IndicatorSoundTestBase::resetAllowAmplifiedVolume() +{ + QProcess proc; + + proc.start("gsettings", QStringList() + << "reset" + << "com.ubuntu.sound" + << "allow-amplified-volume"); + + return runProcess(proc); +} + +bool IndicatorSoundTestBase::runProcess(QProcess& proc) +{ + if (!proc.waitForStarted()) return false; - if (!clearPlayers.waitForFinished()) + if (!proc.waitForFinished()) return false; - return clearPlayers.exitCode() == 0; + return proc.exitCode() == 0; } bool IndicatorSoundTestBase::startTestMprisPlayer(QString const& playerName) diff --git a/tests/integration/indicator-sound-test-base.h b/tests/integration/indicator-sound-test-base.h index 67f347f..d9d3289 100644 --- a/tests/integration/indicator-sound-test-base.h +++ b/tests/integration/indicator-sound-test-base.h @@ -77,6 +77,8 @@ protected: void startAccountsService(); bool clearGSettingsPlayers(); + bool resetAllowAmplifiedVolume(); + bool runProcess(QProcess&); bool startTestMprisPlayer(QString const& playerName); diff --git a/tests/integration/test-indicator.cpp b/tests/integration/test-indicator.cpp index 1f357d7..9224b73 100644 --- a/tests/integration/test-indicator.cpp +++ b/tests/integration/test-indicator.cpp @@ -32,7 +32,7 @@ class TestIndicator: public IndicatorSoundTestBase { }; -TEST_F(TestIndicator, PhoneChangeRoleVolume) +TEST_F(TestIndicator, DISABLED_PhoneChangeRoleVolume) { double INITIAL_VOLUME = 0.0; @@ -46,11 +46,11 @@ TEST_F(TestIndicator, PhoneChangeRoleVolume) // start now the indicator, so it picks the new volumes ASSERT_NO_THROW(startIndicator()); - // Generate a random volume + // Generate a random volume in the range [0...0.33] QTime now = QTime::currentTime(); qsrand(now.msec()); - int randInt = qrand() % 100; - double randomVolume = randInt / 100.0; + int randInt = qrand() % 33; + const double randomVolume = randInt / 100.0; QSignalSpy &userAccountsSpy = *signal_spy_volume_changed_; // set an initial volume to the alert role @@ -542,6 +542,7 @@ TEST_F(TestIndicator, DesktopChangeRoleVolume) { double INITIAL_VOLUME = 0.0; + EXPECT_TRUE(resetAllowAmplifiedVolume()); ASSERT_NO_THROW(startAccountsService()); ASSERT_NO_THROW(startPulseDesktop()); @@ -556,11 +557,11 @@ TEST_F(TestIndicator, DesktopChangeRoleVolume) // start now the indicator, so it picks the new volumes ASSERT_NO_THROW(startIndicator()); - // Generate a random volume + // Generate a random volume in the range [0...0.33] QTime now = QTime::currentTime(); qsrand(now.msec()); - int randInt = qrand() % 100; - double randomVolume = randInt / 100.0; + int randInt = qrand() % 33; + const double randomVolume = randInt / 100.0; // play a test sound, it should NOT change the role in the indicator EXPECT_TRUE(startTestSound("multimedia")); diff --git a/tests/notifications-mock.h b/tests/notifications-mock.h index b0f3b74..3cd8570 100644 --- a/tests/notifications-mock.h +++ b/tests/notifications-mock.h @@ -30,7 +30,7 @@ class NotificationsMock DbusTestDbusMockObject * baseobj = nullptr; public: - NotificationsMock (std::vector<std::string> capabilities = {"body", "body-markup", "icon-static", "image/svg+xml", "x-canonical-private-synchronous", "x-canonical-append", "x-canonical-private-icon-only", "x-canonical-truncation", "private-synchronous", "append", "private-icon-only", "truncation"}) { + NotificationsMock (std::vector<std::string> capabilities = {"actions", "body", "body-markup", "icon-static", "image/svg+xml", "x-canonical-private-synchronous", "x-canonical-append", "x-canonical-private-icon-only", "x-canonical-truncation", "private-synchronous", "append", "private-icon-only", "truncation"}) { mock = dbus_test_dbus_mock_new("org.freedesktop.Notifications"); dbus_test_task_set_bus(DBUS_TEST_TASK(mock), DBUS_TEST_SERVICE_BUS_SESSION); dbus_test_task_set_name(DBUS_TEST_TASK(mock), "Notify"); diff --git a/tests/notifications-test.cc b/tests/notifications-test.cc index c5d9770..6f523f1 100644 --- a/tests/notifications-test.cc +++ b/tests/notifications-test.cc @@ -1,5 +1,5 @@ /* - * Copyright © 2015 Canonical Ltd. + * Copyright © 2015-2016 Canonical Ltd. * * 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,8 +15,10 @@ * * Authors: * Ted Gould <ted@canonical.com> + * Charles Kerr <charles.kerr@canonical.com> */ +#include <algorithm> #include <memory> #include <gtest/gtest.h> @@ -105,6 +107,39 @@ class NotificationsTest : public ::testing::Test g_main_loop_unref(loop); } + void loop_until(const std::function<bool()>& test, unsigned int max_ms=50, unsigned int test_interval_ms=10) { + + // g_timeout's callback only allows a single pointer, + // so use a temporary stack struct to wedge everything into one pointer + struct CallbackData { + const std::function<bool()>& test; + const gint64 deadline; + GMainLoop* loop = g_main_loop_new(nullptr, false); + CallbackData (const std::function<bool()>& f, unsigned int max_ms): + test{f}, + deadline{g_get_monotonic_time() + (max_ms*1000)} {} + ~CallbackData() {g_main_loop_unref(loop);} + } data(test, max_ms); + + // tell the timer to stop looping on success or deadline + auto timerfunc = [](gpointer gdata) -> gboolean { + auto& data = *static_cast<CallbackData*>(gdata); + if (!data.test() && (g_get_monotonic_time() < data.deadline)) + return G_SOURCE_CONTINUE; + g_main_loop_quit(data.loop); + return G_SOURCE_REMOVE; + }; + + // start looping + g_timeout_add (std::min(max_ms, test_interval_ms), timerfunc, &data); + g_main_loop_run(data.loop); + } + + void loop_until_notifications(unsigned int max_seconds=1) { + auto test = [this]{ return !notifications->getNotifications().empty(); }; + loop_until(test, max_seconds); + } + static int unref_idle (gpointer user_data) { g_variant_unref(static_cast<GVariant *>(user_data)); return G_SOURCE_REMOVE; @@ -119,18 +154,40 @@ class NotificationsTest : public ::testing::Test return playerList; } - std::shared_ptr<VolumeControl> volumeControlMock () { + std::shared_ptr<IndicatorSoundOptions> optionsMock () { + auto options = std::shared_ptr<IndicatorSoundOptions>( + INDICATOR_SOUND_OPTIONS(options_mock_new()), + [](IndicatorSoundOptions * options){ + g_clear_object(&options); + }); + return options; + } + + std::shared_ptr<VolumeControl> volumeControlMock (const std::shared_ptr<IndicatorSoundOptions>& optionsMock) { auto volumeControl = std::shared_ptr<VolumeControl>( - VOLUME_CONTROL(volume_control_mock_new()), + VOLUME_CONTROL(volume_control_mock_new(optionsMock.get())), [](VolumeControl * control){ g_clear_object(&control); }); return volumeControl; } - std::shared_ptr<IndicatorSoundService> standardService (std::shared_ptr<VolumeControl> volumeControl, std::shared_ptr<MediaPlayerList> playerList) { + std::shared_ptr<VolumeWarning> volumeWarningMock (const std::shared_ptr<IndicatorSoundOptions>& optionsMock) { + auto volumeWarning = std::shared_ptr<VolumeWarning>( + VOLUME_WARNING(volume_warning_mock_new(optionsMock.get())), + [](VolumeWarning * warning){ + g_clear_object(&warning); + }); + return volumeWarning; + } + + std::shared_ptr<IndicatorSoundService> standardService ( + const std::shared_ptr<VolumeControl>& volumeControl, + const std::shared_ptr<MediaPlayerList>& playerList, + const std::shared_ptr<IndicatorSoundOptions>& options, + const std::shared_ptr<VolumeWarning>& warning) { auto soundService = std::shared_ptr<IndicatorSoundService>( - indicator_sound_service_new(playerList.get(), volumeControl.get(), nullptr), + indicator_sound_service_new(playerList.get(), volumeControl.get(), nullptr, options.get(), warning.get()), [](IndicatorSoundService * service){ g_clear_object(&service); }); @@ -165,10 +222,14 @@ class NotificationsTest : public ::testing::Test g_clear_object(&bus); } + }; TEST_F(NotificationsTest, BasicObject) { - auto soundService = standardService(volumeControlMock(), playerListMock()); + auto options = optionsMock(); + auto volumeControl = volumeControlMock(options); + auto volumeWarning = volumeWarningMock(options); + auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning); /* Give some time settle */ loop(50); @@ -177,8 +238,10 @@ TEST_F(NotificationsTest, BasicObject) { } TEST_F(NotificationsTest, VolumeChanges) { - auto volumeControl = volumeControlMock(); - auto soundService = standardService(volumeControl, playerListMock()); + auto options = optionsMock(); + auto volumeControl = volumeControlMock(options); + auto volumeWarning = volumeWarningMock(options); + auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning); /* Set a volume */ notifications->clearNotifications(); @@ -216,8 +279,10 @@ TEST_F(NotificationsTest, VolumeChanges) { } TEST_F(NotificationsTest, StreamChanges) { - auto volumeControl = volumeControlMock(); - auto soundService = standardService(volumeControl, playerListMock()); + auto options = optionsMock(); + auto volumeControl = volumeControlMock(options); + auto volumeWarning = volumeWarningMock(options); + auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning); /* Set a volume */ notifications->clearNotifications(); @@ -228,7 +293,7 @@ TEST_F(NotificationsTest, StreamChanges) { /* Change Streams, no volume change */ notifications->clearNotifications(); - volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "alarm"); + volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_ALARM); setMockVolume(volumeControl, 0.5, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE); loop(50); notev = notifications->getNotifications(); @@ -236,7 +301,7 @@ TEST_F(NotificationsTest, StreamChanges) { /* Change Streams, volume change */ notifications->clearNotifications(); - volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "alert"); + volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_ALERT); setMockVolume(volumeControl, 0.6, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE); loop(50); notev = notifications->getNotifications(); @@ -244,7 +309,7 @@ TEST_F(NotificationsTest, StreamChanges) { /* Change Streams, no volume change, volume up */ notifications->clearNotifications(); - volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "multimedia"); + volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_MULTIMEDIA); setMockVolume(volumeControl, 0.6, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE); loop(50); setMockVolume(volumeControl, 0.65); @@ -254,8 +319,10 @@ TEST_F(NotificationsTest, StreamChanges) { } TEST_F(NotificationsTest, IconTesting) { - auto volumeControl = volumeControlMock(); - auto soundService = standardService(volumeControl, playerListMock()); + auto options = optionsMock(); + auto volumeControl = volumeControlMock(options); + auto volumeWarning = volumeWarningMock(options); + auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning); /* Set an initial volume */ notifications->clearNotifications(); @@ -288,8 +355,10 @@ TEST_F(NotificationsTest, IconTesting) { } TEST_F(NotificationsTest, ServerRestart) { - auto volumeControl = volumeControlMock(); - auto soundService = standardService(volumeControl, playerListMock()); + auto options = optionsMock(); + auto volumeControl = volumeControlMock(options); + auto volumeWarning = volumeWarningMock(options); + auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning); /* Set a volume */ notifications->clearNotifications(); @@ -335,8 +404,10 @@ TEST_F(NotificationsTest, ServerRestart) { } TEST_F(NotificationsTest, HighVolume) { - auto volumeControl = volumeControlMock(); - auto soundService = standardService(volumeControl, playerListMock()); + auto options = optionsMock(); + auto volumeControl = volumeControlMock(options); + auto volumeWarning = volumeWarningMock(options); + auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning); /* Set a volume */ notifications->clearNotifications(); @@ -350,7 +421,7 @@ TEST_F(NotificationsTest, HighVolume) { /* Set high volume with volume change */ notifications->clearNotifications(); - volume_control_mock_set_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), true); + volume_warning_mock_set_high_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), true); setMockVolume(volumeControl, 0.90); loop(50); notev = notifications->getNotifications(); @@ -360,14 +431,14 @@ TEST_F(NotificationsTest, HighVolume) { EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-value-bar-tint"]); /* Move it back */ - volume_control_mock_set_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), false); + volume_warning_mock_set_high_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), false); setMockVolume(volumeControl, 0.50); loop(50); /* Set high volume without level change */ /* NOTE: This can happen if headphones are plugged in */ notifications->clearNotifications(); - volume_control_mock_set_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), TRUE); + volume_warning_mock_set_high_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), true); loop(50); notev = notifications->getNotifications(); ASSERT_EQ(1, notev.size()); @@ -377,8 +448,10 @@ TEST_F(NotificationsTest, HighVolume) { } TEST_F(NotificationsTest, MenuHide) { - auto volumeControl = volumeControlMock(); - auto soundService = standardService(volumeControl, playerListMock()); + auto options = optionsMock(); + auto volumeControl = volumeControlMock(options); + auto volumeWarning = volumeWarningMock(options); + auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning); /* Set a volume */ notifications->clearNotifications(); @@ -406,9 +479,11 @@ TEST_F(NotificationsTest, MenuHide) { EXPECT_EQ(1, notev.size()); } -TEST_F(NotificationsTest, DISABLED_ExtendendVolumeNotification) { - auto volumeControl = volumeControlMock(); - auto soundService = standardService(volumeControl, playerListMock()); +TEST_F(NotificationsTest, ExtendendVolumeNotification) { + auto options = optionsMock(); + auto volumeControl = volumeControlMock(options); + auto volumeWarning = volumeWarningMock(options); + auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning); /* Set a volume */ notifications->clearNotifications(); @@ -424,7 +499,8 @@ TEST_F(NotificationsTest, DISABLED_ExtendendVolumeNotification) { /* Allow an amplified volume */ notifications->clearNotifications(); - //indicator_sound_service_set_allow_amplified_volume(soundService.get(), TRUE); + volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_ALARM); + options_mock_mock_set_max_volume(OPTIONS_MOCK(options.get()), 1.5); loop(50); notev = notifications->getNotifications(); ASSERT_EQ(1, notev.size()); @@ -440,9 +516,112 @@ TEST_F(NotificationsTest, DISABLED_ExtendendVolumeNotification) { /* Put back */ notifications->clearNotifications(); - //indicator_sound_service_set_allow_amplified_volume(soundService.get(), FALSE); + options_mock_mock_set_max_volume(OPTIONS_MOCK(options.get()), 1.0); loop(50); notev = notifications->getNotifications(); ASSERT_EQ(1, notev.size()); EXPECT_GVARIANT_EQ("@i 100", notev[0].hints["value"]); } + +TEST_F(NotificationsTest, TriggerWarning) { + + // Tests all the conditions needed to trigger a volume warning. + // There are many possible combinations, so this test is slow. :P + + const struct { + bool expected; + VolumeControlActiveOutput output; + } test_outputs[] = { + { false, VOLUME_CONTROL_ACTIVE_OUTPUT_SPEAKERS }, + { true, VOLUME_CONTROL_ACTIVE_OUTPUT_HEADPHONES }, + { true, VOLUME_CONTROL_ACTIVE_OUTPUT_BLUETOOTH_HEADPHONES }, + { false, VOLUME_CONTROL_ACTIVE_OUTPUT_BLUETOOTH_SPEAKER }, + { false, VOLUME_CONTROL_ACTIVE_OUTPUT_USB_SPEAKER }, + { true, VOLUME_CONTROL_ACTIVE_OUTPUT_USB_HEADPHONES }, + { false, VOLUME_CONTROL_ACTIVE_OUTPUT_HDMI_SPEAKER }, + { true, VOLUME_CONTROL_ACTIVE_OUTPUT_HDMI_HEADPHONES }, + { false, VOLUME_CONTROL_ACTIVE_OUTPUT_CALL_MODE } + }; + + const struct { + bool expected; + pa_volume_t volume; + pa_volume_t loud_volume; + } test_volumes[] = { + { false, 50, 100 }, + { false, 99, 100 }, + { true, 100, 100 }, + { true, 101, 100 } + }; + + const struct { + bool expected; + bool approved; + } test_approved[] = { + { true, false }, + { false, true } + }; + + const struct { + bool expected; + bool warnings_enabled; + } test_warnings_enabled[] = { + { true, true }, + { false, false } + }; + + const struct { + bool expected; + bool multimedia_active; + } test_multimedia_active[] = { + { true, true }, + { false, false } + }; + + for (const auto& outputs : test_outputs) { + for (const auto& volumes : test_volumes) { + for (const auto& approved : test_approved) { + for (const auto& warnings_enabled : test_warnings_enabled) { + for (const auto& multimedia_active : test_multimedia_active) { + + notifications->clearNotifications(); + + // instantiate the test subjects + auto options = optionsMock(); + auto volumeControl = volumeControlMock(options); + auto volumeWarning = volumeWarningMock(options); + auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning); + + // run the test + options_mock_mock_set_loud_volume(OPTIONS_MOCK(options.get()), volumes.loud_volume); + options_mock_mock_set_loud_warning_enabled(OPTIONS_MOCK(options.get()), warnings_enabled.warnings_enabled); + volume_warning_mock_set_approved(VOLUME_WARNING_MOCK(volumeWarning.get()), approved.approved); + volume_warning_mock_set_multimedia_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), volumes.volume); + volume_warning_mock_set_multimedia_active(VOLUME_WARNING_MOCK(volumeWarning.get()), multimedia_active.multimedia_active); + volume_control_mock_mock_set_active_output(VOLUME_CONTROL_MOCK(volumeControl.get()), outputs.output); + + loop_until_notifications(); + + // check the result + auto notev = notifications->getNotifications(); + const bool warning_expected = outputs.expected && volumes.expected && approved.expected && warnings_enabled.expected && multimedia_active.expected; + if (warning_expected) { + EXPECT_TRUE(volume_warning_get_active(volumeWarning.get())); + ASSERT_EQ(1, notev.size()); + EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-snap-decisions"]); + EXPECT_GVARIANT_EQ(nullptr, notev[0].hints["x-canonical-private-synchronous"]); + } + else { + EXPECT_FALSE(volume_warning_get_active(volumeWarning.get())); + ASSERT_EQ(1, notev.size()); + EXPECT_GVARIANT_EQ(nullptr, notev[0].hints["x-canonical-snap-decisions"]); + EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-private-synchronous"]); + } + + } // multimedia_active + } // warnings_enabled + } // approved + } // volumes + } // outputs +} + diff --git a/tests/options-mock.vala b/tests/options-mock.vala new file mode 100644 index 0000000..cda9e24 --- /dev/null +++ b/tests/options-mock.vala @@ -0,0 +1,28 @@ +/* + * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*- + * Copyright © 2015 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +public class OptionsMock : IndicatorSound.Options +{ + public void mock_set_max_volume (double val) { max_volume = val; } + + public void mock_set_loud_volume (PulseAudio.Volume val) { loud_volume = val; } + + public void mock_set_loud_warning_enabled (bool val) { loud_warning_enabled = val; } +} diff --git a/tests/pa-mock.cpp b/tests/pa-mock.cpp index 5dd5c9b..7e7b70c 100644 --- a/tests/pa-mock.cpp +++ b/tests/pa-mock.cpp @@ -351,6 +351,25 @@ pa_context_set_source_volume_by_name (pa_context *c, const char * name, const pa return dummy_operation(); } +pa_operation* +pa_context_get_sink_input_info_list(pa_context *c, pa_sink_input_info_cb_t cb, void *userdata) +{ + reinterpret_cast<PAMockContext*>(c)->idleOnce( + [c, cb, userdata]() { + + pa_sink_input_info sink_input; + sink_input.name = "default-sink-input"; + sink_input.proplist = nullptr; + sink_input.has_volume = false; + + if (cb != nullptr) + cb(c, &sink_input, true, userdata); + }); + + return dummy_operation(); +} + + /* ******************************* * subscribe.h * *******************************/ diff --git a/tests/volume-control-mock.vala b/tests/volume-control-mock.vala index dc11fba..159df3b 100644 --- a/tests/volume-control-mock.vala +++ b/tests/volume-control-mock.vala @@ -20,27 +20,33 @@ public class VolumeControlMock : VolumeControl { - private bool _high_volume = false; - public override bool high_volume { get { return _high_volume; } protected set { _high_volume = value; } } - public void set_high_volume(bool b) { high_volume = b; } - - public string mock_stream { get; set; default = "multimedia"; } - public override string stream { get { return mock_stream; } } - public override bool ready { get; set; } + public void mock_set_is_ready(bool b) { ready = b; } + public void mock_set_active_stream(VolumeControl.Stream s) { active_stream = s; } + public void mock_set_is_playing(bool b) { is_playing = b; } public override bool active_mic { get; set; } public bool mock_mute { get; set; } public override bool mute { get { return mock_mute; } } - public bool mock_is_playing { get; set; } - public override bool is_playing { get { return mock_is_playing; } } private VolumeControl.Volume _vol = new VolumeControl.Volume(); public override VolumeControl.Volume volume { get { return _vol; } set { _vol = value; }} public override double mic_volume { get; set; } public override void set_mute (bool mute) { - } - public VolumeControlMock() { + private VolumeControl.ActiveOutput _active_output = VolumeControl.ActiveOutput.SPEAKERS; + + public override VolumeControl.ActiveOutput active_output() { + return _active_output; + } + + public void mock_set_active_output (VolumeControl.ActiveOutput val) { + _active_output = val; + this.active_output_changed(val); + } + + public VolumeControlMock(IndicatorSound.Options options) { + base(options); + ready = true; this.notify["mock-stream"].connect(() => this.notify_property("stream")); this.notify["mock-high-volume"].connect(() => this.notify_property("high-volume")); diff --git a/tests/volume-control-test.cc b/tests/volume-control-test.cc index 5022245..11fa4ff 100644 --- a/tests/volume-control-test.cc +++ b/tests/volume-control-test.cc @@ -23,6 +23,7 @@ extern "C" { #include "indicator-sound-service.h" +#include "vala-mocks.h" } class VolumeControlTest : public ::testing::Test @@ -75,7 +76,9 @@ class VolumeControlTest : public ::testing::Test }; TEST_F(VolumeControlTest, BasicObject) { - VolumeControlPulse * control = volume_control_pulse_new(); + auto options = options_mock_new(); + auto pgloop = pa_glib_mainloop_new(NULL); + auto control = volume_control_pulse_new(INDICATOR_SOUND_OPTIONS(options), pgloop); /* Setup the PA backend */ loop(100); @@ -84,4 +87,6 @@ TEST_F(VolumeControlTest, BasicObject) { EXPECT_TRUE(volume_control_get_ready(VOLUME_CONTROL(control))); g_clear_object(&control); + g_clear_object(&options); + g_clear_pointer(&pgloop, pa_glib_mainloop_free); } diff --git a/tests/volume-warning-mock.vala b/tests/volume-warning-mock.vala new file mode 100644 index 0000000..f69688f --- /dev/null +++ b/tests/volume-warning-mock.vala @@ -0,0 +1,40 @@ +/* + * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*- + * Copyright © 2015 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Ted Gould <ted@canonical.com> + */ + +public class VolumeWarningMock : VolumeWarning +{ + public void set_high_volume(bool val) { high_volume = val; } + + public VolumeWarningMock(IndicatorSound.Options options) { + base(options); + } + + protected override void sound_system_set_multimedia_volume(PulseAudio.Volume volume) { + GLib.message("volume-warning-mock setting multimedia volume to %d", (int)volume); + } + + public void set_multimedia_active(bool val) { multimedia_active = val; } + + public void set_multimedia_volume(PulseAudio.Volume val) { multimedia_volume = val; } + + public void set_approved(bool val) { approved = val; } + + public bool is_approved() { return approved; } +} |