aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTed Gould <ted@gould.cx>2015-02-19 16:27:24 +0000
committerCI Train Bot <ci-train-bot@canonical.com>2015-02-19 16:27:24 +0000
commit14be74296b0bd81a797373c4c972916277d0428e (patch)
tree6833db57605bf225620214218a427fe0f0a4444b
parent403caf562ba967342b78d5491229fe40cc908361 (diff)
parent18891aa50808f29365f763cc503e5dfa2e17ab17 (diff)
downloadayatana-indicator-sound-14be74296b0bd81a797373c4c972916277d0428e.tar.gz
ayatana-indicator-sound-14be74296b0bd81a797373c4c972916277d0428e.tar.bz2
ayatana-indicator-sound-14be74296b0bd81a797373c4c972916277d0428e.zip
Add notifications mock and tests
Approved by: Jussi Pakkanen, PS Jenkins bot
-rw-r--r--src/CMakeLists.txt6
-rw-r--r--src/main.c76
-rw-r--r--src/media-player-list.vala2
-rw-r--r--src/service.vala158
-rw-r--r--src/sound-menu.vala13
-rw-r--r--src/volume-control-pulse.vala917
-rw-r--r--src/volume-control.vala910
-rw-r--r--tests/CMakeLists.txt26
-rw-r--r--tests/gtest-gvariant.h110
-rw-r--r--tests/indicator-test.cc6
-rw-r--r--tests/media-player-list-mock.vala25
-rw-r--r--tests/notifications-mock.h155
-rw-r--r--tests/notifications-test.cc349
-rw-r--r--tests/volume-control-mock.vala47
-rw-r--r--tests/volume-control-test.cc4
15 files changed, 1814 insertions, 990 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b6f006a..479d01a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -43,6 +43,7 @@ vala_add(indicator-sound-service
DEPENDS
sound-menu
volume-control
+ volume-control-pulse
media-player
media-player-list
mpris2-interfaces
@@ -52,6 +53,11 @@ vala_add(indicator-sound-service
volume-control.vala
)
vala_add(indicator-sound-service
+ volume-control-pulse.vala
+ DEPENDS
+ volume-control
+)
+vala_add(indicator-sound-service
media-player.vala
)
vala_add(indicator-sound-service
diff --git a/src/main.c b/src/main.c
index e9a148e..ad8b3d4 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,6 +1,18 @@
-/* main.c generated by valac 0.22.1, the Vala compiler
- * generated from main.vala, do not modify */
-
+/*
+ * 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/>.
+ */
#include <glib.h>
#include <locale.h>
@@ -9,33 +21,77 @@
#include "indicator-sound-service.h"
#include "config.h"
+static gboolean
+sigterm_handler (gpointer data)
+{
+ g_debug("Got SIGTERM");
+ g_main_loop_quit((GMainLoop *)data);
+ return G_SOURCE_REMOVE;
+}
+
+static void
+name_lost (GDBusConnection * connection, const gchar * name, gpointer user_data)
+{
+ g_debug("Name lost");
+ g_main_loop_quit((GMainLoop *)user_data);
+}
+
int
main (int argc, char ** argv) {
- gint result = 0;
+ GMainLoop * loop = NULL;
IndicatorSoundService* service = NULL;
+ GDBusConnection * bus = NULL;
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
setlocale (LC_ALL, "");
bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
+ /* Grab DBus */
+ GError * error = NULL;
+ bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
+ if (error != NULL) {
+ g_error("Unable to get session bus: %s", error->message);
+ g_error_free(error);
+ return -1;
+ }
+
+ /* Build Mainloop */
+ loop = g_main_loop_new(NULL, FALSE);
+
+ g_unix_signal_add(SIGTERM, sigterm_handler, loop);
+
/* Initialize libnotify */
notify_init ("indicator-sound");
MediaPlayerList * playerlist = NULL;
+ AccountsServiceUser * accounts = NULL;
if (g_strcmp0("lightdm", g_get_user_name()) == 0) {
playerlist = MEDIA_PLAYER_LIST(media_player_list_greeter_new());
} else {
playerlist = MEDIA_PLAYER_LIST(media_player_list_mpris_new());
+ accounts = accounts_service_user_new();
}
- service = indicator_sound_service_new (playerlist);
- result = indicator_sound_service_run (service);
+ VolumeControlPulse * volume = volume_control_pulse_new();
- g_object_unref(playerlist);
- g_object_unref(service);
+ service = indicator_sound_service_new (playerlist, volume, accounts);
- return result;
-}
+ g_bus_own_name_on_connection(bus,
+ "com.canonical.indicator.sound",
+ G_BUS_NAME_OWNER_FLAGS_NONE,
+ NULL, /* acquired */
+ name_lost,
+ loop,
+ NULL);
+ g_main_loop_run(loop);
+
+ g_clear_object(&playerlist);
+ g_clear_object(&accounts);
+ g_clear_object(&service);
+ g_clear_object(&bus);
+
+ return 0;
+}
diff --git a/src/media-player-list.vala b/src/media-player-list.vala
index fadbf63..03cd7d6 100644
--- a/src/media-player-list.vala
+++ b/src/media-player-list.vala
@@ -17,7 +17,7 @@
* Ted Gould <ted@canonical.com>
*/
-public class MediaPlayerList {
+public abstract class MediaPlayerList : Object {
public class Iterator {
public virtual MediaPlayer? next_value() {
return null;
diff --git a/src/service.vala b/src/service.vala
index b07f267..95f8f16 100644
--- a/src/service.vala
+++ b/src/service.vala
@@ -18,13 +18,29 @@
*/
public class IndicatorSound.Service: Object {
- public Service (MediaPlayerList playerlist) {
+ DBusConnection bus;
+ DBusProxy notification_proxy;
+
+ public Service (MediaPlayerList playerlist, VolumeControl volume, AccountsServiceUser? accounts) {
+ try {
+ bus = Bus.get_sync(GLib.BusType.SESSION);
+ } catch (GLib.Error e) {
+ error("Unable to get DBus session bus: %s", e.message);
+ }
+
sync_notification = new Notify.Notification(_("Volume"), "", "audio-volume-muted");
- this.notification_server_watch = GLib.Bus.watch_name(GLib.BusType.SESSION,
- "org.freedesktop.Notifications",
- GLib.BusNameWatcherFlags.NONE,
- () => { check_sync_notification = false; },
- () => { check_sync_notification = false; });
+ try {
+ this.notification_proxy = new DBusProxy.for_bus_sync(GLib.BusType.SESSION,
+ DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | DBusProxyFlags.DO_NOT_CONNECT_SIGNALS | DBusProxyFlags.DO_NOT_AUTO_START,
+ null, /* interface info */
+ "org.freedesktop.Notifications",
+ "/org/freedesktop/Notifications",
+ "org.freedesktop.Notifications",
+ null);
+ this.notification_proxy.notify["g-name-owner"].connect ( () => { debug("Notifications name owner changed"); check_sync_notification = false; } );
+ } catch (GLib.Error e) {
+ error("Unable to build notification proxy: %s", e.message);
+ }
this.settings = new Settings ("com.canonical.indicator.sound");
this.sharedsettings = new Settings ("com.ubuntu.sound");
@@ -32,12 +48,11 @@ public class IndicatorSound.Service: Object {
this.settings.bind ("visible", this, "visible", SettingsBindFlags.GET);
this.notify["visible"].connect ( () => this.update_root_icon () );
- this.volume_control = new VolumeControl ();
+ this.volume_control = volume;
+ this.accounts_service = accounts;
/* If we're on the greeter, don't export */
- if (GLib.Environment.get_user_name() != "lightdm") {
- this.accounts_service = new AccountsServiceUser();
-
+ if (this.accounts_service != null) {
this.accounts_service.notify["showDataOnGreeter"].connect(() => {
this.export_to_accounts_service = this.accounts_service.showDataOnGreeter;
eventually_update_player_actions();
@@ -90,17 +105,35 @@ public class IndicatorSound.Service: Object {
}
}
});
+
+ /* Everything is built, let's put it on the bus */
+ try {
+ export_actions = bus.export_action_group ("/com/canonical/indicator/sound", this.actions);
+ } catch (Error e) {
+ critical ("%s", e.message);
+ }
+
+ this.menus.@foreach ( (profile, menu) => menu.export (bus, @"/com/canonical/indicator/sound/$profile"));
}
~Service() {
+ debug("Destroying Service Object");
+
+ clear_acts_player();
+
+ if (this.player_action_update_id > 0) {
+ Source.remove (this.player_action_update_id);
+ this.player_action_update_id = 0;
+ }
+
if (this.sound_was_blocked_timeout_id > 0) {
Source.remove (this.sound_was_blocked_timeout_id);
this.sound_was_blocked_timeout_id = 0;
}
- if (this.notification_server_watch != 0) {
- GLib.Bus.unwatch_name(this.notification_server_watch);
- this.notification_server_watch = 0;
+ if (this.export_actions != 0) {
+ bus.unexport_action_group(this.export_actions);
+ this.export_actions = 0;
}
}
@@ -115,30 +148,6 @@ public class IndicatorSound.Service: Object {
this.accounts_service.player = null;
}
- public int run () {
- if (this.loop != null) {
- warning ("service is already running");
- return 1;
- }
-
- Bus.own_name (BusType.SESSION, "com.canonical.indicator.sound", BusNameOwnerFlags.NONE,
- this.bus_acquired, null, this.name_lost);
-
- this.loop = new MainLoop (null, false);
-
- GLib.Unix.signal_add(GLib.ProcessSignal.TERM, () => {
- debug("SIGTERM recieved, stopping our mainloop");
- this.loop.quit();
- return false;
- });
-
- this.loop.run ();
-
- clear_acts_player();
-
- return 0;
- }
-
public bool visible { get; set; }
public bool allow_amplified_volume {
@@ -171,7 +180,6 @@ public class IndicatorSound.Service: Object {
{ "indicator-shown", null, null, "@b false", null },
};
- MainLoop loop;
SimpleActionGroup actions;
HashTable<string, SoundMenu> menus;
Settings settings;
@@ -185,7 +193,6 @@ public class IndicatorSound.Service: Object {
AccountsServiceUser? accounts_service = null;
bool export_to_accounts_service = false;
private Notify.Notification sync_notification;
- private uint notification_server_watch;
/* Maximum volume as a scaling factor between the volume action's state and the value in
* this.volume_control. See create_volume_action().
@@ -275,6 +282,7 @@ public class IndicatorSound.Service: Object {
void update_sync_notification () {
if (!check_sync_notification) {
+ support_sync_notification = false;
List<string> caps = Notify.get_server_caps ();
if (caps.find_custom ("x-canonical-private-synchronous", strcmp) != null) {
support_sync_notification = true;
@@ -285,21 +293,6 @@ public class IndicatorSound.Service: Object {
if (!support_sync_notification)
return;
- /* Update our volume and output */
- var oldoutput = this.last_output_notification;
- this.last_output_notification = this.volume_control.stream;
-
- var oldvolume = this.last_volume_notification;
- this.last_volume_notification = volume_control.volume;
-
- /* Suppress notifications of volume changes if it is because the
- output stream changed. */
- if (oldoutput != this.last_output_notification)
- return;
- /* Supress updates that don't change the value */
- if (GLib.Math.fabs(oldvolume - this.last_volume_notification) < 0.01)
- return;
-
var shown_action = actions.lookup_action ("indicator-shown") as SimpleAction;
if (shown_action != null && shown_action.get_state().get_boolean())
return;
@@ -341,13 +334,14 @@ public class IndicatorSound.Service: Object {
}
}
+ SimpleAction silent_action;
Action create_silent_mode_action () {
bool silentNow = false;
if (this.accounts_service != null) {
silentNow = this.accounts_service.silentMode;
}
- var silent_action = new SimpleAction.stateful ("silent-mode", null, new Variant.boolean (silentNow));
+ silent_action = new SimpleAction.stateful ("silent-mode", null, new Variant.boolean (silentNow));
/* If we're not dealing with accounts service, we'll just always be out
of silent mode and that's cool. */
@@ -371,8 +365,9 @@ public class IndicatorSound.Service: Object {
return silent_action;
}
+ SimpleAction mute_action;
Action create_mute_action () {
- var mute_action = new SimpleAction.stateful ("mute", null, new Variant.boolean (this.volume_control.mute));
+ mute_action = new SimpleAction.stateful ("mute", null, new Variant.boolean (this.volume_control.mute));
mute_action.activate.connect ( (action, param) => {
action.change_state (new Variant.boolean (!action.get_state ().get_boolean ()));
@@ -415,6 +410,7 @@ public class IndicatorSound.Service: Object {
return mute_action;
}
+ SimpleAction volume_action;
Action create_volume_action () {
/* The action's state is between be in [0.0, 1.0] instead of [0.0,
* max_volume], so that we don't need to update the slider menu item
@@ -425,7 +421,7 @@ public class IndicatorSound.Service: Object {
double volume = this.volume_control.volume / this.max_volume;
- var volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, new Variant.double (volume));
+ volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, new Variant.double (volume));
volume_action.change_state.connect ( (action, val) => {
double v = val.get_double () * this.max_volume;
@@ -440,12 +436,26 @@ public class IndicatorSound.Service: Object {
});
this.volume_control.notify["volume"].connect (() => {
- var vol_action = this.actions.lookup_action ("volume") as SimpleAction;
-
/* Normalize volume, because the volume action's state is [0.0, 1.0], see create_volume_action() */
- vol_action.set_state (new Variant.double (this.volume_control.volume / this.max_volume));
+ volume_action.set_state (new Variant.double (this.volume_control.volume / this.max_volume));
this.update_root_icon ();
+
+ /* Update our volume and output */
+ var oldoutput = this.last_output_notification;
+ this.last_output_notification = this.volume_control.stream;
+
+ var oldvolume = this.last_volume_notification;
+ this.last_volume_notification = volume_control.volume;
+
+ /* Suppress notifications of volume changes if it is because the
+ output stream changed. */
+ if (oldoutput != this.last_output_notification)
+ return;
+ /* Supress updates that don't change the value */
+ if (GLib.Math.fabs(oldvolume - this.last_volume_notification) < 0.01)
+ return;
+
this.update_sync_notification ();
});
@@ -454,24 +464,26 @@ public class IndicatorSound.Service: Object {
return volume_action;
}
+ SimpleAction mic_volume_action;
Action create_mic_volume_action () {
- var volume_action = new SimpleAction.stateful ("mic-volume", null, new Variant.double (this.volume_control.mic_volume));
+ mic_volume_action = new SimpleAction.stateful ("mic-volume", null, new Variant.double (this.volume_control.mic_volume));
- volume_action.change_state.connect ( (action, val) => {
+ mic_volume_action.change_state.connect ( (action, val) => {
volume_control.mic_volume = val.get_double ();
});
this.volume_control.notify["mic-volume"].connect ( () => {
- volume_action.set_state (new Variant.double (this.volume_control.mic_volume));
+ mic_volume_action.set_state (new Variant.double (this.volume_control.mic_volume));
});
- this.volume_control.bind_property ("ready", volume_action, "enabled", BindingFlags.SYNC_CREATE);
+ this.volume_control.bind_property ("ready", mic_volume_action, "enabled", BindingFlags.SYNC_CREATE);
- return volume_action;
+ return mic_volume_action;
}
+ SimpleAction high_volume_action;
Action create_high_volume_action () {
- var 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, new Variant.boolean (this.volume_control.high_volume));
this.volume_control.notify["high-volume"].connect( () => {
high_volume_action.set_state(new Variant.boolean (this.volume_control.high_volume));
@@ -481,19 +493,7 @@ public class IndicatorSound.Service: Object {
return high_volume_action;
}
- void bus_acquired (DBusConnection connection, string name) {
- try {
- connection.export_action_group ("/com/canonical/indicator/sound", this.actions);
- } catch (Error e) {
- critical ("%s", e.message);
- }
-
- this.menus.@foreach ( (profile, menu) => menu.export (connection, @"/com/canonical/indicator/sound/$profile"));
- }
-
- void name_lost (DBusConnection connection, string name) {
- this.loop.quit ();
- }
+ uint export_actions = 0;
Variant action_state_for_player (MediaPlayer player, bool show_track = true) {
var builder = new VariantBuilder (new VariantType ("a{sv}"));
diff --git a/src/sound-menu.vala b/src/sound-menu.vala
index 96dd143..8718162 100644
--- a/src/sound-menu.vala
+++ b/src/sound-menu.vala
@@ -73,9 +73,20 @@ public class SoundMenu: Object
this.greeter_players = (flags & DisplayFlags.GREETER_PLAYERS) != 0;
}
+ ~SoundMenu () {
+ if (export_id != 0) {
+ bus.unexport_menu_model(export_id);
+ export_id = 0;
+ }
+ }
+
+ DBusConnection? bus = null;
+ uint export_id = 0;
+
public void export (DBusConnection connection, string object_path) {
+ bus = connection;
try {
- connection.export_menu_model (object_path, this.root);
+ export_id = bus.export_menu_model (object_path, this.root);
} catch (Error e) {
critical ("%s", e.message);
}
diff --git a/src/volume-control-pulse.vala b/src/volume-control-pulse.vala
new file mode 100644
index 0000000..1e81ce1
--- /dev/null
+++ b/src/volume-control-pulse.vala
@@ -0,0 +1,917 @@
+/*
+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
+ * Copyright 2013 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:
+ * Alberto Ruiz <alberto.ruiz@canonical.com>
+ */
+
+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
+{
+ public abstract async string get_active_entry () throws IOError;
+ public signal void entry_selected (string entry_name);
+}
+
+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 uint _reconnect_timer = 0;
+
+ private PulseAudio.Context context;
+ private bool _mute = true;
+ private bool _is_playing = false;
+ private double _volume = 0.0;
+ private double _mic_volume = 0.0;
+
+ /* Used by the pulseaudio stream restore extension */
+ private DBusConnection _pconn;
+ /* Need both the list and hash so we can retrieve the last known sink-input after
+ * releasing the current active one (restoring back to the previous known role) */
+ private Gee.ArrayList<uint32> _sink_input_list = new Gee.ArrayList<uint32> ();
+ private HashMap<uint32, string> _sink_input_hash = new HashMap<uint32, string> ();
+ private bool _pulse_use_stream_restore = false;
+ private uint32 _active_sink_input = -1;
+ private string[] _valid_roles = {"multimedia", "alert", "alarm", "phone"};
+ public override string stream {
+ get {
+ if (_active_sink_input < 0 || _active_sink_input >= _valid_roles.length)
+ return "multimedia";
+ else
+ return _valid_roles[_active_sink_input];
+ }
+ }
+ private string? _objp_role_multimedia = null;
+ private string? _objp_role_alert = null;
+ private string? _objp_role_alarm = null;
+ private string? _objp_role_phone = null;
+ private uint _pa_volume_sig_count = 0;
+
+ private DBusProxy _user_proxy;
+ private GreeterListInterface _greeter_proxy;
+ private Cancellable _mute_cancellable;
+ private Cancellable _volume_cancellable;
+ private uint _local_volume_timer = 0;
+ private uint _accountservice_volume_timer = 0;
+ private bool _send_next_local_volume = false;
+ private double _account_service_volume = 0.0;
+ private bool _active_port_headphone = false;
+
+ /** 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; }
+
+ /** true when high volume warnings should be shown */
+ public override bool high_volume {
+ get {
+ return this._volume > 0.75 && _active_port_headphone;
+ }
+ }
+
+ public VolumeControlPulse ()
+ {
+ if (loop == null)
+ loop = new PulseAudio.GLibMainLoop ();
+
+ _mute_cancellable = new Cancellable ();
+ _volume_cancellable = new Cancellable ();
+
+ setup_accountsservice.begin ();
+
+ this.reconnect_to_pulse ();
+ }
+
+ ~VolumeControlPulse ()
+ {
+ if (_reconnect_timer != 0) {
+ Source.remove (_reconnect_timer);
+ _reconnect_timer = 0;
+ }
+ stop_local_volume_timer();
+ stop_account_service_volume_timer();
+ }
+
+ /* PulseAudio logic*/
+ private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index)
+ {
+ switch (t & Context.SubscriptionEventType.FACILITY_MASK)
+ {
+ case Context.SubscriptionEventType.SINK:
+ update_sink ();
+ break;
+
+ case Context.SubscriptionEventType.SINK_INPUT:
+ switch (t & Context.SubscriptionEventType.TYPE_MASK)
+ {
+ case Context.SubscriptionEventType.NEW:
+ c.get_sink_input_info (index, handle_new_sink_input_cb);
+ break;
+
+ case Context.SubscriptionEventType.CHANGE:
+ c.get_sink_input_info (index, handle_changed_sink_input_cb);
+ break;
+
+ case Context.SubscriptionEventType.REMOVE:
+ remove_sink_input_from_list (index);
+ break;
+ default:
+ debug ("Sink input event not known.");
+ break;
+ }
+ break;
+
+ case Context.SubscriptionEventType.SOURCE:
+ update_source ();
+ break;
+
+ case Context.SubscriptionEventType.SOURCE_OUTPUT:
+ switch (t & Context.SubscriptionEventType.TYPE_MASK)
+ {
+ case Context.SubscriptionEventType.NEW:
+ c.get_source_output_info (index, source_output_info_cb);
+ break;
+
+ case Context.SubscriptionEventType.REMOVE:
+ this.active_mic = false;
+ break;
+ }
+ break;
+ }
+ }
+
+ private void sink_info_cb_for_props (Context c, SinkInfo? i, int eol)
+ {
+ bool old_high_volume = this.high_volume;
+
+ if (i == null)
+ return;
+
+ if (_mute != (bool)i.mute)
+ {
+ _mute = (bool)i.mute;
+ this.notify_property ("mute");
+ }
+
+ var playing = (i.state == PulseAudio.SinkState.RUNNING);
+ if (_is_playing != playing)
+ {
+ _is_playing = playing;
+ this.notify_property ("is-playing");
+ }
+
+ /* Check if the current active port is headset/headphone */
+ /* There is not easy way to check if the port is a headset/headphone besides
+ * checking for the port name. On touch (with the pulseaudio droid element)
+ * the headset/headphone port is called 'output-headset' and 'output-headphone'.
+ * On the desktop this is usually called 'analog-output-headphones' */
+ if (i.active_port != null &&
+ (i.active_port.name == "output-wired_headset" ||
+ i.active_port.name == "output-wired_headphone" ||
+ i.active_port.name == "analog-output-headphones")) {
+ _active_port_headphone = true;
+ } else {
+ _active_port_headphone = false;
+ }
+
+ if (_pulse_use_stream_restore == false &&
+ _volume != volume_to_double (i.volume.max ()))
+ {
+ _volume = volume_to_double (i.volume.max ());
+ this.notify_property("volume");
+ start_local_volume_timer();
+ }
+
+ if (this.high_volume != old_high_volume) {
+ this.notify_property("high-volume");
+ }
+ }
+
+ private void source_info_cb (Context c, SourceInfo? i, int eol)
+ {
+ if (i == null)
+ return;
+
+ if (_mic_volume != volume_to_double (i.volume.values[0]))
+ {
+ _mic_volume = volume_to_double (i.volume.values[0]);
+ this.notify_property ("mic-volume");
+ }
+ }
+
+ private void server_info_cb_for_props (Context c, ServerInfo? i)
+ {
+ if (i == null)
+ return;
+ context.get_sink_info_by_name (i.default_sink_name, sink_info_cb_for_props);
+ }
+
+ private void update_sink ()
+ {
+ context.get_server_info (server_info_cb_for_props);
+ }
+
+ private void update_source_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) {
+ if (i != null)
+ context.get_source_info_by_name (i.default_source_name, source_info_cb);
+ }
+
+ private void update_source ()
+ {
+ context.get_server_info (update_source_get_server_info_cb);
+ }
+
+ private DBusMessage pulse_dbus_filter (DBusConnection connection, owned DBusMessage message, bool incoming)
+ {
+ if (message.get_message_type () == DBusMessageType.SIGNAL) {
+ string active_role_objp = _objp_role_alert;
+ if (_active_sink_input != -1)
+ active_role_objp = _sink_input_hash.get (_active_sink_input);
+
+ if (message.get_path () == active_role_objp && message.get_member () == "VolumeUpdated") {
+ uint sig_count = 0;
+ lock (_pa_volume_sig_count) {
+ sig_count = _pa_volume_sig_count;
+ if (_pa_volume_sig_count > 0)
+ _pa_volume_sig_count--;
+ }
+
+ /* We only care about signals if our internal count is zero */
+ if (sig_count == 0) {
+ /* Extract volume and make sure it's not a side effect of us setting it */
+ Variant body = message.get_body ();
+ Variant varray = body.get_child_value (0);
+
+ uint32 type = 0, volume = 0;
+ VariantIter iter = varray.iterator ();
+ iter.next ("(uu)", &type, &volume);
+ /* Here we need to compare integer values to avoid rounding issues, so just
+ * using the volume values used by pulseaudio */
+ PulseAudio.Volume cvolume = double_to_volume (_volume);
+ if (volume != cvolume) {
+ /* Someone else changed the volume for this role, reflect on the indicator */
+ _volume = volume_to_double (volume);
+ this.notify_property("volume");
+ start_local_volume_timer();
+ }
+ }
+ }
+ }
+
+ return message;
+ }
+
+ private async void update_active_sink_input (uint32 index)
+ {
+ if ((index == -1) || (index != _active_sink_input && index in _sink_input_list)) {
+ string sink_input_objp = _objp_role_alert;
+ if (index != -1)
+ sink_input_objp = _sink_input_hash.get (index);
+ _active_sink_input = index;
+
+ /* Listen for role volume changes from pulse itself (external clients) */
+ try {
+ var builder = new VariantBuilder (new VariantType ("ao"));
+ builder.add ("o", sink_input_objp);
+
+ yield _pconn.call ("org.PulseAudio.Core1", "/org/pulseaudio/core1",
+ "org.PulseAudio.Core1", "ListenForSignal",
+ new Variant ("(sao)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry.VolumeUpdated", builder),
+ null, DBusCallFlags.NONE, -1);
+ } catch (GLib.Error e) {
+ warning ("unable to listen for pulseaudio dbus signals (%s)", e.message);
+ }
+
+ try {
+ var props_variant = yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry",
+ sink_input_objp, "org.freedesktop.DBus.Properties", "Get",
+ new Variant ("(ss)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume"),
+ null, DBusCallFlags.NONE, -1);
+ Variant tmp;
+ props_variant.get ("(v)", out tmp);
+ uint32 type = 0, volume = 0;
+ VariantIter iter = tmp.iterator ();
+ iter.next ("(uu)", &type, &volume);
+
+ _volume = volume_to_double (volume);
+ this.notify_property("volume");
+ start_local_volume_timer();
+ } catch (GLib.Error e) {
+ warning ("unable to get volume for active role %s (%s)", sink_input_objp, e.message);
+ }
+ }
+ }
+
+ private void add_sink_input_into_list (SinkInputInfo sink_input)
+ {
+ /* We're only adding ones that are not corked and with a valid role */
+ var role = sink_input.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
+
+ if (role != null && role in _valid_roles) {
+ if (sink_input.corked == 0 || role == "phone") {
+ _sink_input_list.insert (0, sink_input.index);
+ switch (role)
+ {
+ case "multimedia":
+ _sink_input_hash.set (sink_input.index, _objp_role_multimedia);
+ break;
+ case "alert":
+ _sink_input_hash.set (sink_input.index, _objp_role_alert);
+ break;
+ case "alarm":
+ _sink_input_hash.set (sink_input.index, _objp_role_alarm);
+ break;
+ case "phone":
+ _sink_input_hash.set (sink_input.index, _objp_role_phone);
+ break;
+ }
+ /* Only switch the active sink input in case a phone one is not active */
+ if (_active_sink_input == -1 ||
+ _sink_input_hash.get (_active_sink_input) != _objp_role_phone)
+ update_active_sink_input.begin (sink_input.index);
+ }
+ }
+ }
+
+ private void remove_sink_input_from_list (uint32 index)
+ {
+ if (index in _sink_input_list) {
+ _sink_input_list.remove (index);
+ _sink_input_hash.unset (index);
+ if (index == _active_sink_input) {
+ if (_sink_input_list.size != 0)
+ update_active_sink_input.begin (_sink_input_list.get (0));
+ else
+ update_active_sink_input.begin (-1);
+ }
+ }
+ }
+
+ private void handle_new_sink_input_cb (Context c, SinkInputInfo? i, int eol)
+ {
+ if (i == null)
+ return;
+
+ add_sink_input_into_list (i);
+ }
+
+ private void handle_changed_sink_input_cb (Context c, SinkInputInfo? i, int eol)
+ {
+ if (i == null)
+ return;
+
+ if (i.index in _sink_input_list) {
+ /* Phone stream is always corked, so handle it differently */
+ if (i.corked == 1 && _sink_input_hash.get (i.index) != _objp_role_phone)
+ remove_sink_input_from_list (i.index);
+ } else {
+ if (i.corked == 0)
+ add_sink_input_into_list (i);
+ }
+ }
+
+ private void source_output_info_cb (Context c, SourceOutputInfo? i, int eol)
+ {
+ if (i == null)
+ return;
+
+ var role = i.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
+ if (role == "phone" || role == "production")
+ this.active_mic = true;
+ }
+
+ private void context_state_callback (Context c)
+ {
+ switch (c.get_state ()) {
+ case Context.State.READY:
+ if (_pulse_use_stream_restore) {
+ c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
+ PulseAudio.Context.SubscriptionMask.SINK_INPUT |
+ PulseAudio.Context.SubscriptionMask.SOURCE |
+ PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
+ } else {
+ c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
+ PulseAudio.Context.SubscriptionMask.SOURCE |
+ PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
+ }
+ c.set_subscribe_callback (context_events_cb);
+ update_sink ();
+ update_source ();
+ this.ready = true;
+ break;
+
+ case Context.State.FAILED:
+ case Context.State.TERMINATED:
+ if (_reconnect_timer == 0)
+ _reconnect_timer = Timeout.add_seconds (2, reconnect_timeout);
+ break;
+
+ default:
+ this.ready = false;
+ break;
+ }
+ }
+
+ bool reconnect_timeout ()
+ {
+ _reconnect_timer = 0;
+ reconnect_to_pulse ();
+ return false; // G_SOURCE_REMOVE
+ }
+
+ void reconnect_to_pulse ()
+ {
+ if (this.ready) {
+ this.context.disconnect ();
+ this.context = null;
+ this.ready = false;
+ }
+
+ var props = new Proplist ();
+ props.sets (Proplist.PROP_APPLICATION_NAME, "Ubuntu Audio Settings");
+ props.sets (Proplist.PROP_APPLICATION_ID, "com.canonical.settings.sound");
+ props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control");
+ props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1");
+
+ reconnect_pulse_dbus ();
+
+ this.context = new PulseAudio.Context (loop.get_api(), null, props);
+ this.context.set_state_callback (context_state_callback);
+
+ if (context.connect(null, Context.Flags.NOFAIL, null) < 0)
+ warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno()));
+ }
+
+ void sink_info_list_callback_set_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) {
+ if (sink != null)
+ context.set_sink_mute_by_index (sink.index, true, null);
+ }
+
+ void sink_info_list_callback_unset_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) {
+ if (sink != null)
+ context.set_sink_mute_by_index (sink.index, false, null);
+ }
+
+ /* Mute operations */
+ bool set_mute_internal (bool mute)
+ {
+ return_val_if_fail (context.get_state () == Context.State.READY, false);
+
+ if (_mute != mute) {
+ if (mute)
+ context.get_sink_info_list (sink_info_list_callback_set_mute);
+ else
+ context.get_sink_info_list (sink_info_list_callback_unset_mute);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public override void set_mute (bool mute)
+ {
+ if (set_mute_internal (mute))
+ sync_mute_to_accountsservice.begin (mute);
+ }
+
+ public void toggle_mute ()
+ {
+ this.set_mute (!this._mute);
+ }
+
+ public override bool mute
+ {
+ get
+ {
+ return this._mute;
+ }
+ }
+
+ public override bool is_playing
+ {
+ get
+ {
+ return this._is_playing;
+ }
+ }
+
+ /* Volume operations */
+ private 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)
+ {
+ double tmp = (double)(vol - PulseAudio.Volume.MUTED);
+ return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED);
+ }
+
+ private void set_volume_success_cb (Context c, int success)
+ {
+ if ((bool)success)
+ this.notify_property("volume");
+ }
+
+ private void sink_info_set_volume_cb (Context c, SinkInfo? i, int eol)
+ {
+ if (i == null)
+ return;
+
+ unowned CVolume cvol = i.volume;
+ cvol.scale (double_to_volume (_volume));
+ c.set_sink_volume_by_index (i.index, cvol, set_volume_success_cb);
+ }
+
+ private void server_info_cb_for_set_volume (Context c, ServerInfo? i)
+ {
+ if (i == null)
+ {
+ warning ("Could not get PulseAudio server info");
+ return;
+ }
+
+ context.get_sink_info_by_name (i.default_sink_name, sink_info_set_volume_cb);
+ }
+
+ private async void set_volume_active_role ()
+ {
+ string active_role_objp = _objp_role_alert;
+
+ if (_active_sink_input != -1 && _active_sink_input in _sink_input_list)
+ active_role_objp = _sink_input_hash.get (_active_sink_input);
+
+ try {
+ var builder = new VariantBuilder (new VariantType ("a(uu)"));
+ builder.add ("(uu)", 0, double_to_volume (_volume));
+ Variant volume = builder.end ();
+
+ /* Increase the signal counter so we can handle the callback */
+ lock (_pa_volume_sig_count) {
+ _pa_volume_sig_count++;
+ }
+
+ yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry",
+ active_role_objp, "org.freedesktop.DBus.Properties", "Set",
+ new Variant ("(ssv)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume", volume),
+ null, DBusCallFlags.NONE, -1);
+
+ this.notify_property("volume");
+ } catch (GLib.Error e) {
+ lock (_pa_volume_sig_count) {
+ _pa_volume_sig_count--;
+ }
+ warning ("unable to set volume for stream obj path %s (%s)", active_role_objp, e.message);
+ }
+ }
+
+ bool set_volume_internal (double volume)
+ {
+ if (context.get_state () != Context.State.READY)
+ return false;
+
+ if (_volume != volume) {
+ var old_high_volume = this.high_volume;
+
+ _volume = volume;
+ if (_pulse_use_stream_restore)
+ set_volume_active_role.begin ();
+ else
+ context.get_server_info (server_info_cb_for_set_volume);
+
+ this.notify_property("volume");
+
+ if (this.high_volume != old_high_volume)
+ this.notify_property("high-volume");
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ void set_mic_volume_success_cb (Context c, int success)
+ {
+ if ((bool)success)
+ this.notify_property ("mic-volume");
+ }
+
+ void set_mic_volume_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) {
+ if (i != null) {
+ unowned CVolume cvol = CVolume ();
+ cvol = vol_set (cvol, 1, double_to_volume (_mic_volume));
+ c.set_source_volume_by_name (i.default_source_name, cvol, set_mic_volume_success_cb);
+ }
+ }
+
+ public override double volume {
+ get {
+ return _volume;
+ }
+ set {
+ if (set_volume_internal (value)) {
+ start_local_volume_timer();
+ }
+ }
+ }
+
+ public override double mic_volume {
+ get {
+ return _mic_volume;
+ }
+ set {
+ return_if_fail (context.get_state () == Context.State.READY);
+
+ _mic_volume = value;
+
+ context.get_server_info (set_mic_volume_get_server_info_cb);
+ }
+ }
+
+ /* PulseAudio Dbus (Stream Restore) logic */
+ private void reconnect_pulse_dbus ()
+ {
+ 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 {
+ DBusConnection conn;
+ Variant props;
+
+ try {
+ conn = Bus.get_sync (BusType.SESSION);
+ } catch (GLib.IOError e) {
+ warning ("unable to get the dbus session bus: %s", e.message);
+ return;
+ }
+
+ try {
+ var props_variant = conn.call_sync ("org.PulseAudio1",
+ "/org/pulseaudio/server_lookup1", "org.freedesktop.DBus.Properties",
+ "Get", new Variant ("(ss)", "org.PulseAudio.ServerLookup1", "Address"),
+ null, DBusCallFlags.NONE, -1);
+ props_variant.get ("(v)", out props);
+ address = props.get_string ();
+ } catch (GLib.Error e) {
+ warning ("unable to get pulse unix socket: %s", e.message);
+ return;
+ }
+ }
+
+ stdout.printf ("PulseAudio dbus unix socket: %s\n", address);
+ try {
+ _pconn = new DBusConnection.for_address_sync (address, DBusConnectionFlags.AUTHENTICATION_CLIENT);
+ } catch (GLib.Error e) {
+ /* If it fails, it means the dbus pulse extension is not available */
+ 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");
+
+ /* 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) {
+ stdout.printf ("Using PulseAudio DBUS Stream Restore module\n");
+ /* Restore volume and update default entry */
+ update_active_sink_input.begin (-1);
+ _pulse_use_stream_restore = true;
+ }
+ }
+
+ private string? stream_restore_get_object_path (string name) {
+ string? objp = null;
+ try {
+ Variant props_variant = _pconn.call_sync ("org.PulseAudio.Ext.StreamRestore1",
+ "/org/pulseaudio/stream_restore1", "org.PulseAudio.Ext.StreamRestore1",
+ "GetEntryByName", new Variant ("(s)", name), null, DBusCallFlags.NONE, -1);
+ /* Workaround for older versions of vala that don't provide get_objv */
+ VariantIter iter = props_variant.iterator ();
+ iter.next ("o", &objp);
+ stdout.printf ("Found obj path %s for restore data named %s\n", objp, name);
+ } catch (GLib.Error e) {
+ warning ("unable to find stream restore data for: %s", name);
+ }
+ return objp;
+ }
+
+ /* 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"));
+ if (volume_variant != null) {
+ var volume = volume_variant.get_double ();
+ if (volume >= 0) {
+ _account_service_volume = volume;
+ // we need to wait for this to settle.
+ start_account_service_volume_timer();
+ }
+ }
+
+ Variant mute_variant = changed_properties.lookup_value ("Muted", new VariantType ("b"));
+ if (mute_variant != null) {
+ var mute = mute_variant.get_boolean ();
+ set_mute_internal (mute);
+ }
+ }
+
+ private async void setup_user_proxy (string? username_in = null)
+ {
+ var username = username_in;
+ _user_proxy = null;
+
+ // Look up currently selected greeter user, if asked
+ if (username == null) {
+ try {
+ username = yield _greeter_proxy.get_active_entry ();
+ if (username == "" || username == null)
+ return;
+ } catch (GLib.Error e) {
+ warning ("unable to find Accounts path for user %s: %s", username, e.message);
+ return;
+ }
+ }
+
+ // Get master AccountsService object
+ DBusProxy accounts_proxy;
+ try {
+ accounts_proxy = yield DBusProxy.create_for_bus (BusType.SYSTEM, DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, null, "org.freedesktop.Accounts", "/org/freedesktop/Accounts", "org.freedesktop.Accounts");
+ } catch (GLib.Error e) {
+ warning ("unable to get greeter proxy: %s", e.message);
+ return;
+ }
+
+ // Find user's AccountsService object
+ try {
+ var user_path_variant = yield accounts_proxy.call ("FindUserByName", new Variant ("(s)", username), DBusCallFlags.NONE, -1);
+ string user_path;
+ user_path_variant.get ("(o)", out user_path);
+ _user_proxy = yield DBusProxy.create_for_bus (BusType.SYSTEM, DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, "org.freedesktop.Accounts", user_path, "com.ubuntu.AccountsService.Sound");
+ } catch (GLib.Error e) {
+ warning ("unable to find Accounts path for user %s: %s", username, e.message);
+ return;
+ }
+
+ // Get current values and listen for changes
+ _user_proxy.g_properties_changed.connect (accountsservice_props_changed_cb);
+ try {
+ var props_variant = yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "GetAll", new Variant ("(s)", _user_proxy.get_interface_name ()), null, DBusCallFlags.NONE, -1);
+ Variant props;
+ props_variant.get ("(@a{sv})", out props);
+ accountsservice_props_changed_cb(_user_proxy, props, null);
+ } catch (GLib.Error e) {
+ debug("Unable to get properties for user %s at first try: %s", username, e.message);
+ }
+ }
+
+ private void greeter_user_changed (string username)
+ {
+ setup_user_proxy.begin (username);
+ }
+
+ private async void setup_accountsservice ()
+ {
+ if (Environment.get_variable ("XDG_SESSION_CLASS") == "greeter") {
+ try {
+ _greeter_proxy = yield Bus.get_proxy (BusType.SESSION, "com.canonical.UnityGreeter", "/list");
+ } catch (GLib.Error e) {
+ warning ("unable to get greeter proxy: %s", e.message);
+ return;
+ }
+ _greeter_proxy.entry_selected.connect (greeter_user_changed);
+ yield setup_user_proxy ();
+ } else {
+ // We are in a user session. We just need our own proxy
+ var username = Environment.get_variable ("USER");
+ if (username != "" && username != null) {
+ yield setup_user_proxy (username);
+ }
+ }
+ }
+
+ private async void sync_mute_to_accountsservice (bool mute)
+ {
+ if (_user_proxy == null)
+ return;
+
+ _mute_cancellable.cancel ();
+ _mute_cancellable.reset ();
+
+ try {
+ yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "Set", new Variant ("(ssv)", _user_proxy.get_interface_name (), "Muted", new Variant ("b", mute)), null, DBusCallFlags.NONE, -1, _mute_cancellable);
+ } catch (GLib.Error e) {
+ warning ("unable to sync mute to AccountsService: %s", e.message);
+ }
+ }
+
+ private async void sync_volume_to_accountsservice (double volume)
+ {
+ if (_user_proxy == null)
+ return;
+
+ _volume_cancellable.cancel ();
+ _volume_cancellable.reset ();
+
+ try {
+ yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "Set", new Variant ("(ssv)", _user_proxy.get_interface_name (), "Volume", new Variant ("d", volume)), null, DBusCallFlags.NONE, -1, _volume_cancellable);
+ } catch (GLib.Error e) {
+ warning ("unable to sync volume to AccountsService: %s", e.message);
+ }
+ }
+
+ private void start_local_volume_timer()
+ {
+ // perform a slow sync with the accounts service. max at 1 per second.
+
+ // stop the AS update timer, as since we're going to be setting the volume.
+ stop_account_service_volume_timer();
+
+ if (_local_volume_timer == 0) {
+ sync_volume_to_accountsservice.begin (_volume);
+ _local_volume_timer = Timeout.add_seconds (1, local_volume_changed_timeout);
+ } else {
+ _send_next_local_volume = true;
+ }
+ }
+
+ private void stop_local_volume_timer()
+ {
+ if (_local_volume_timer != 0) {
+ Source.remove (_local_volume_timer);
+ _local_volume_timer = 0;
+ }
+ }
+
+ bool local_volume_changed_timeout()
+ {
+ _local_volume_timer = 0;
+ if (_send_next_local_volume) {
+ _send_next_local_volume = false;
+ start_local_volume_timer ();
+ }
+ return false; // G_SOURCE_REMOVE
+ }
+
+ private void start_account_service_volume_timer()
+ {
+ if (_accountservice_volume_timer == 0) {
+ // If we haven't been messing with local volume recently, apply immediately.
+ if (_local_volume_timer == 0 && !set_volume_internal (_account_service_volume)) {
+ return;
+ }
+ // Else check again in another second if needed.
+ // (if AS is throwing us lots of notifications, we update at most once a second)
+ _accountservice_volume_timer = Timeout.add_seconds (1, accountservice_volume_changed_timeout);
+ }
+ }
+
+ private void stop_account_service_volume_timer()
+ {
+ if (_accountservice_volume_timer != 0) {
+ Source.remove (_accountservice_volume_timer);
+ _accountservice_volume_timer = 0;
+ }
+ }
+
+ bool accountservice_volume_changed_timeout ()
+ {
+ _accountservice_volume_timer = 0;
+ start_account_service_volume_timer ();
+ return false; // G_SOURCE_REMOVE
+ }
+}
diff --git a/src/volume-control.vala b/src/volume-control.vala
index 62cb2d0..b06ea56 100644
--- a/src/volume-control.vala
+++ b/src/volume-control.vala
@@ -1,6 +1,6 @@
/*
* -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
- * Copyright 2013 Canonical Ltd.
+ * 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
@@ -15,903 +15,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
- * Alberto Ruiz <alberto.ruiz@canonical.com>
+ * Ted Gould <ted@canonical.com>
*/
-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
-{
- public abstract async string get_active_entry () throws IOError;
- public signal void entry_selected (string entry_name);
-}
-
-public class VolumeControl : Object
+public abstract class VolumeControl : Object
{
- /* this is static to ensure it being freed after @context (loop does not have ref counting) */
- private static PulseAudio.GLibMainLoop loop;
-
- private uint _reconnect_timer = 0;
-
- private PulseAudio.Context context;
- private bool _mute = true;
- private bool _is_playing = false;
- private double _volume = 0.0;
- private double _mic_volume = 0.0;
-
- /* Used by the pulseaudio stream restore extension */
- private DBusConnection _pconn;
- /* Need both the list and hash so we can retrieve the last known sink-input after
- * releasing the current active one (restoring back to the previous known role) */
- private Gee.ArrayList<uint32> _sink_input_list = new Gee.ArrayList<uint32> ();
- private HashMap<uint32, string> _sink_input_hash = new HashMap<uint32, string> ();
- private bool _pulse_use_stream_restore = false;
- private uint32 _active_sink_input = -1;
- private string[] _valid_roles = {"multimedia", "alert", "alarm", "phone"};
- public string stream {
- get {
- if (_active_sink_input < 0 || _active_sink_input >= _valid_roles.length)
- return "multimedia";
- else
- return _valid_roles[_active_sink_input];
- }
- }
- private string? _objp_role_multimedia = null;
- private string? _objp_role_alert = null;
- private string? _objp_role_alarm = null;
- private string? _objp_role_phone = null;
- private uint _pa_volume_sig_count = 0;
-
- private DBusProxy _user_proxy;
- private GreeterListInterface _greeter_proxy;
- private Cancellable _mute_cancellable;
- private Cancellable _volume_cancellable;
- private uint _local_volume_timer = 0;
- private uint _accountservice_volume_timer = 0;
- private bool _send_next_local_volume = false;
- private double _account_service_volume = 0.0;
- private bool _active_port_headphone = false;
-
- /** true when connected to the pulse server */
- public bool ready { get; set; }
-
- /** true when a microphone is active **/
- public bool active_mic { get; private set; default = false; }
-
- /** true when high volume warnings should be shown */
- public bool high_volume {
- get {
- return this._volume > 0.75 && _active_port_headphone;
- }
- }
-
- public VolumeControl ()
- {
- if (loop == null)
- loop = new PulseAudio.GLibMainLoop ();
-
- _mute_cancellable = new Cancellable ();
- _volume_cancellable = new Cancellable ();
-
- setup_accountsservice.begin ();
-
- this.reconnect_to_pulse ();
- }
-
- ~VolumeControl ()
- {
- if (_reconnect_timer != 0) {
- Source.remove (_reconnect_timer);
- _reconnect_timer = 0;
- }
- stop_local_volume_timer();
- stop_account_service_volume_timer();
- }
-
- /* PulseAudio logic*/
- private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index)
- {
- switch (t & Context.SubscriptionEventType.FACILITY_MASK)
- {
- case Context.SubscriptionEventType.SINK:
- update_sink ();
- break;
-
- case Context.SubscriptionEventType.SINK_INPUT:
- switch (t & Context.SubscriptionEventType.TYPE_MASK)
- {
- case Context.SubscriptionEventType.NEW:
- c.get_sink_input_info (index, handle_new_sink_input_cb);
- break;
-
- case Context.SubscriptionEventType.CHANGE:
- c.get_sink_input_info (index, handle_changed_sink_input_cb);
- break;
-
- case Context.SubscriptionEventType.REMOVE:
- remove_sink_input_from_list (index);
- break;
- default:
- debug ("Sink input event not known.");
- break;
- }
- break;
-
- case Context.SubscriptionEventType.SOURCE:
- update_source ();
- break;
-
- case Context.SubscriptionEventType.SOURCE_OUTPUT:
- switch (t & Context.SubscriptionEventType.TYPE_MASK)
- {
- case Context.SubscriptionEventType.NEW:
- c.get_source_output_info (index, source_output_info_cb);
- break;
-
- case Context.SubscriptionEventType.REMOVE:
- this.active_mic = false;
- break;
- }
- break;
- }
- }
-
- private void sink_info_cb_for_props (Context c, SinkInfo? i, int eol)
- {
- bool old_high_volume = this.high_volume;
-
- if (i == null)
- return;
-
- if (_mute != (bool)i.mute)
- {
- _mute = (bool)i.mute;
- this.notify_property ("mute");
- }
-
- var playing = (i.state == PulseAudio.SinkState.RUNNING);
- if (_is_playing != playing)
- {
- _is_playing = playing;
- this.notify_property ("is-playing");
- }
-
- /* Check if the current active port is headset/headphone */
- /* There is not easy way to check if the port is a headset/headphone besides
- * checking for the port name. On touch (with the pulseaudio droid element)
- * the headset/headphone port is called 'output-headset' and 'output-headphone'.
- * On the desktop this is usually called 'analog-output-headphones' */
- if (i.active_port != null &&
- (i.active_port.name == "output-wired_headset" ||
- i.active_port.name == "output-wired_headphone" ||
- i.active_port.name == "analog-output-headphones")) {
- _active_port_headphone = true;
- } else {
- _active_port_headphone = false;
- }
-
- if (_pulse_use_stream_restore == false &&
- _volume != volume_to_double (i.volume.max ()))
- {
- _volume = volume_to_double (i.volume.max ());
- this.notify_property("volume");
- start_local_volume_timer();
- }
-
- if (this.high_volume != old_high_volume) {
- this.notify_property("high-volume");
- }
- }
-
- private void source_info_cb (Context c, SourceInfo? i, int eol)
- {
- if (i == null)
- return;
-
- if (_mic_volume != volume_to_double (i.volume.values[0]))
- {
- _mic_volume = volume_to_double (i.volume.values[0]);
- this.notify_property ("mic-volume");
- }
- }
-
- private void server_info_cb_for_props (Context c, ServerInfo? i)
- {
- if (i == null)
- return;
- context.get_sink_info_by_name (i.default_sink_name, sink_info_cb_for_props);
- }
-
- private void update_sink ()
- {
- context.get_server_info (server_info_cb_for_props);
- }
-
- private void update_source_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) {
- if (i != null)
- context.get_source_info_by_name (i.default_source_name, source_info_cb);
- }
-
- private void update_source ()
- {
- context.get_server_info (update_source_get_server_info_cb);
- }
-
- private DBusMessage pulse_dbus_filter (DBusConnection connection, owned DBusMessage message, bool incoming)
- {
- if (message.get_message_type () == DBusMessageType.SIGNAL) {
- string active_role_objp = _objp_role_alert;
- if (_active_sink_input != -1)
- active_role_objp = _sink_input_hash.get (_active_sink_input);
-
- if (message.get_path () == active_role_objp && message.get_member () == "VolumeUpdated") {
- uint sig_count = 0;
- lock (_pa_volume_sig_count) {
- sig_count = _pa_volume_sig_count;
- if (_pa_volume_sig_count > 0)
- _pa_volume_sig_count--;
- }
-
- /* We only care about signals if our internal count is zero */
- if (sig_count == 0) {
- /* Extract volume and make sure it's not a side effect of us setting it */
- Variant body = message.get_body ();
- Variant varray = body.get_child_value (0);
-
- uint32 type = 0, volume = 0;
- VariantIter iter = varray.iterator ();
- iter.next ("(uu)", &type, &volume);
- /* Here we need to compare integer values to avoid rounding issues, so just
- * using the volume values used by pulseaudio */
- PulseAudio.Volume cvolume = double_to_volume (_volume);
- if (volume != cvolume) {
- /* Someone else changed the volume for this role, reflect on the indicator */
- _volume = volume_to_double (volume);
- this.notify_property("volume");
- start_local_volume_timer();
- }
- }
- }
- }
-
- return message;
- }
-
- private async void update_active_sink_input (uint32 index)
- {
- if ((index == -1) || (index != _active_sink_input && index in _sink_input_list)) {
- string sink_input_objp = _objp_role_alert;
- if (index != -1)
- sink_input_objp = _sink_input_hash.get (index);
- _active_sink_input = index;
-
- /* Listen for role volume changes from pulse itself (external clients) */
- try {
- var builder = new VariantBuilder (new VariantType ("ao"));
- builder.add ("o", sink_input_objp);
-
- yield _pconn.call ("org.PulseAudio.Core1", "/org/pulseaudio/core1",
- "org.PulseAudio.Core1", "ListenForSignal",
- new Variant ("(sao)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry.VolumeUpdated", builder),
- null, DBusCallFlags.NONE, -1);
- } catch (GLib.Error e) {
- warning ("unable to listen for pulseaudio dbus signals (%s)", e.message);
- }
-
- try {
- var props_variant = yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry",
- sink_input_objp, "org.freedesktop.DBus.Properties", "Get",
- new Variant ("(ss)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume"),
- null, DBusCallFlags.NONE, -1);
- Variant tmp;
- props_variant.get ("(v)", out tmp);
- uint32 type = 0, volume = 0;
- VariantIter iter = tmp.iterator ();
- iter.next ("(uu)", &type, &volume);
-
- _volume = volume_to_double (volume);
- this.notify_property("volume");
- start_local_volume_timer();
- } catch (GLib.Error e) {
- warning ("unable to get volume for active role %s (%s)", sink_input_objp, e.message);
- }
- }
- }
-
- private void add_sink_input_into_list (SinkInputInfo sink_input)
- {
- /* We're only adding ones that are not corked and with a valid role */
- var role = sink_input.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
-
- if (role != null && role in _valid_roles) {
- if (sink_input.corked == 0 || role == "phone") {
- _sink_input_list.insert (0, sink_input.index);
- switch (role)
- {
- case "multimedia":
- _sink_input_hash.set (sink_input.index, _objp_role_multimedia);
- break;
- case "alert":
- _sink_input_hash.set (sink_input.index, _objp_role_alert);
- break;
- case "alarm":
- _sink_input_hash.set (sink_input.index, _objp_role_alarm);
- break;
- case "phone":
- _sink_input_hash.set (sink_input.index, _objp_role_phone);
- break;
- }
- /* Only switch the active sink input in case a phone one is not active */
- if (_active_sink_input == -1 ||
- _sink_input_hash.get (_active_sink_input) != _objp_role_phone)
- update_active_sink_input.begin (sink_input.index);
- }
- }
- }
-
- private void remove_sink_input_from_list (uint32 index)
- {
- if (index in _sink_input_list) {
- _sink_input_list.remove (index);
- _sink_input_hash.unset (index);
- if (index == _active_sink_input) {
- if (_sink_input_list.size != 0)
- update_active_sink_input.begin (_sink_input_list.get (0));
- else
- update_active_sink_input.begin (-1);
- }
- }
- }
-
- private void handle_new_sink_input_cb (Context c, SinkInputInfo? i, int eol)
- {
- if (i == null)
- return;
-
- add_sink_input_into_list (i);
- }
-
- private void handle_changed_sink_input_cb (Context c, SinkInputInfo? i, int eol)
- {
- if (i == null)
- return;
-
- if (i.index in _sink_input_list) {
- /* Phone stream is always corked, so handle it differently */
- if (i.corked == 1 && _sink_input_hash.get (i.index) != _objp_role_phone)
- remove_sink_input_from_list (i.index);
- } else {
- if (i.corked == 0)
- add_sink_input_into_list (i);
- }
- }
-
- private void source_output_info_cb (Context c, SourceOutputInfo? i, int eol)
- {
- if (i == null)
- return;
-
- var role = i.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
- if (role == "phone" || role == "production")
- this.active_mic = true;
- }
-
- private void context_state_callback (Context c)
- {
- switch (c.get_state ()) {
- case Context.State.READY:
- if (_pulse_use_stream_restore) {
- c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
- PulseAudio.Context.SubscriptionMask.SINK_INPUT |
- PulseAudio.Context.SubscriptionMask.SOURCE |
- PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
- } else {
- c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
- PulseAudio.Context.SubscriptionMask.SOURCE |
- PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
- }
- c.set_subscribe_callback (context_events_cb);
- update_sink ();
- update_source ();
- this.ready = true;
- break;
-
- case Context.State.FAILED:
- case Context.State.TERMINATED:
- if (_reconnect_timer == 0)
- _reconnect_timer = Timeout.add_seconds (2, reconnect_timeout);
- break;
-
- default:
- this.ready = false;
- break;
- }
- }
-
- bool reconnect_timeout ()
- {
- _reconnect_timer = 0;
- reconnect_to_pulse ();
- return false; // G_SOURCE_REMOVE
- }
-
- void reconnect_to_pulse ()
- {
- if (this.ready) {
- this.context.disconnect ();
- this.context = null;
- this.ready = false;
- }
-
- var props = new Proplist ();
- props.sets (Proplist.PROP_APPLICATION_NAME, "Ubuntu Audio Settings");
- props.sets (Proplist.PROP_APPLICATION_ID, "com.canonical.settings.sound");
- props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control");
- props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1");
-
- reconnect_pulse_dbus ();
-
- this.context = new PulseAudio.Context (loop.get_api(), null, props);
- this.context.set_state_callback (context_state_callback);
-
- if (context.connect(null, Context.Flags.NOFAIL, null) < 0)
- warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno()));
- }
-
- void sink_info_list_callback_set_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) {
- if (sink != null)
- context.set_sink_mute_by_index (sink.index, true, null);
- }
-
- void sink_info_list_callback_unset_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) {
- if (sink != null)
- context.set_sink_mute_by_index (sink.index, false, null);
- }
-
- /* Mute operations */
- bool set_mute_internal (bool mute)
- {
- return_val_if_fail (context.get_state () == Context.State.READY, false);
-
- if (_mute != mute) {
- if (mute)
- context.get_sink_info_list (sink_info_list_callback_set_mute);
- else
- context.get_sink_info_list (sink_info_list_callback_unset_mute);
- return true;
- } else {
- return false;
- }
- }
-
- public void set_mute (bool mute)
- {
- if (set_mute_internal (mute))
- sync_mute_to_accountsservice.begin (mute);
- }
-
- public void toggle_mute ()
- {
- this.set_mute (!this._mute);
- }
-
- public bool mute
- {
- get
- {
- return this._mute;
- }
- }
-
- public bool is_playing
- {
- get
- {
- return this._is_playing;
- }
- }
-
- /* Volume operations */
- private 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)
- {
- double tmp = (double)(vol - PulseAudio.Volume.MUTED);
- return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED);
- }
-
- private void set_volume_success_cb (Context c, int success)
- {
- if ((bool)success)
- this.notify_property("volume");
- }
-
- private void sink_info_set_volume_cb (Context c, SinkInfo? i, int eol)
- {
- if (i == null)
- return;
-
- unowned CVolume cvol = i.volume;
- cvol.scale (double_to_volume (_volume));
- c.set_sink_volume_by_index (i.index, cvol, set_volume_success_cb);
- }
-
- private void server_info_cb_for_set_volume (Context c, ServerInfo? i)
- {
- if (i == null)
- {
- warning ("Could not get PulseAudio server info");
- return;
- }
-
- context.get_sink_info_by_name (i.default_sink_name, sink_info_set_volume_cb);
- }
-
- private async void set_volume_active_role ()
- {
- string active_role_objp = _objp_role_alert;
-
- if (_active_sink_input != -1 && _active_sink_input in _sink_input_list)
- active_role_objp = _sink_input_hash.get (_active_sink_input);
-
- try {
- var builder = new VariantBuilder (new VariantType ("a(uu)"));
- builder.add ("(uu)", 0, double_to_volume (_volume));
- Variant volume = builder.end ();
-
- /* Increase the signal counter so we can handle the callback */
- lock (_pa_volume_sig_count) {
- _pa_volume_sig_count++;
- }
-
- yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry",
- active_role_objp, "org.freedesktop.DBus.Properties", "Set",
- new Variant ("(ssv)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume", volume),
- null, DBusCallFlags.NONE, -1);
-
- this.notify_property("volume");
- } catch (GLib.Error e) {
- lock (_pa_volume_sig_count) {
- _pa_volume_sig_count--;
- }
- warning ("unable to set volume for stream obj path %s (%s)", active_role_objp, e.message);
- }
- }
-
- bool set_volume_internal (double volume)
- {
- if (context.get_state () != Context.State.READY)
- return false;
-
- if (_volume != volume) {
- var old_high_volume = this.high_volume;
-
- _volume = volume;
- if (_pulse_use_stream_restore)
- set_volume_active_role.begin ();
- else
- context.get_server_info (server_info_cb_for_set_volume);
-
- this.notify_property("volume");
-
- if (this.high_volume != old_high_volume)
- this.notify_property("high-volume");
-
- return true;
- } else {
- return false;
- }
- }
-
- void set_mic_volume_success_cb (Context c, int success)
- {
- if ((bool)success)
- this.notify_property ("mic-volume");
- }
-
- void set_mic_volume_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) {
- if (i != null) {
- unowned CVolume cvol = CVolume ();
- cvol = vol_set (cvol, 1, double_to_volume (_mic_volume));
- c.set_source_volume_by_name (i.default_source_name, cvol, set_mic_volume_success_cb);
- }
- }
-
- public double volume {
- get {
- return _volume;
- }
- set {
- if (set_volume_internal (value)) {
- start_local_volume_timer();
- }
- }
- }
-
- public double mic_volume {
- get {
- return _mic_volume;
- }
- set {
- return_if_fail (context.get_state () == Context.State.READY);
-
- _mic_volume = value;
-
- context.get_server_info (set_mic_volume_get_server_info_cb);
- }
- }
-
- /* PulseAudio Dbus (Stream Restore) logic */
- private void reconnect_pulse_dbus ()
- {
- 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 {
- DBusConnection conn;
- Variant props;
-
- try {
- conn = Bus.get_sync (BusType.SESSION);
- } catch (GLib.IOError e) {
- warning ("unable to get the dbus session bus: %s", e.message);
- return;
- }
-
- try {
- var props_variant = conn.call_sync ("org.PulseAudio1",
- "/org/pulseaudio/server_lookup1", "org.freedesktop.DBus.Properties",
- "Get", new Variant ("(ss)", "org.PulseAudio.ServerLookup1", "Address"),
- null, DBusCallFlags.NONE, -1);
- props_variant.get ("(v)", out props);
- address = props.get_string ();
- } catch (GLib.Error e) {
- warning ("unable to get pulse unix socket: %s", e.message);
- return;
- }
- }
-
- stdout.printf ("PulseAudio dbus unix socket: %s\n", address);
- try {
- _pconn = new DBusConnection.for_address_sync (address, DBusConnectionFlags.AUTHENTICATION_CLIENT);
- } catch (GLib.Error e) {
- /* If it fails, it means the dbus pulse extension is not available */
- 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");
-
- /* 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) {
- stdout.printf ("Using PulseAudio DBUS Stream Restore module\n");
- /* Restore volume and update default entry */
- update_active_sink_input.begin (-1);
- _pulse_use_stream_restore = true;
- }
- }
-
- private string? stream_restore_get_object_path (string name) {
- string? objp = null;
- try {
- Variant props_variant = _pconn.call_sync ("org.PulseAudio.Ext.StreamRestore1",
- "/org/pulseaudio/stream_restore1", "org.PulseAudio.Ext.StreamRestore1",
- "GetEntryByName", new Variant ("(s)", name), null, DBusCallFlags.NONE, -1);
- /* Workaround for older versions of vala that don't provide get_objv */
- VariantIter iter = props_variant.iterator ();
- iter.next ("o", &objp);
- stdout.printf ("Found obj path %s for restore data named %s\n", objp, name);
- } catch (GLib.Error e) {
- warning ("unable to find stream restore data for: %s", name);
- }
- return objp;
- }
-
- /* 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"));
- if (volume_variant != null) {
- var volume = volume_variant.get_double ();
- if (volume >= 0) {
- _account_service_volume = volume;
- // we need to wait for this to settle.
- start_account_service_volume_timer();
- }
- }
-
- Variant mute_variant = changed_properties.lookup_value ("Muted", new VariantType ("b"));
- if (mute_variant != null) {
- var mute = mute_variant.get_boolean ();
- set_mute_internal (mute);
- }
- }
-
- private async void setup_user_proxy (string? username_in = null)
- {
- var username = username_in;
- _user_proxy = null;
-
- // Look up currently selected greeter user, if asked
- if (username == null) {
- try {
- username = yield _greeter_proxy.get_active_entry ();
- if (username == "" || username == null)
- return;
- } catch (GLib.Error e) {
- warning ("unable to find Accounts path for user %s: %s", username, e.message);
- return;
- }
- }
-
- // Get master AccountsService object
- DBusProxy accounts_proxy;
- try {
- accounts_proxy = yield DBusProxy.create_for_bus (BusType.SYSTEM, DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, null, "org.freedesktop.Accounts", "/org/freedesktop/Accounts", "org.freedesktop.Accounts");
- } catch (GLib.Error e) {
- warning ("unable to get greeter proxy: %s", e.message);
- return;
- }
-
- // Find user's AccountsService object
- try {
- var user_path_variant = yield accounts_proxy.call ("FindUserByName", new Variant ("(s)", username), DBusCallFlags.NONE, -1);
- string user_path;
- user_path_variant.get ("(o)", out user_path);
- _user_proxy = yield DBusProxy.create_for_bus (BusType.SYSTEM, DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, "org.freedesktop.Accounts", user_path, "com.ubuntu.AccountsService.Sound");
- } catch (GLib.Error e) {
- warning ("unable to find Accounts path for user %s: %s", username, e.message);
- return;
- }
-
- // Get current values and listen for changes
- _user_proxy.g_properties_changed.connect (accountsservice_props_changed_cb);
- try {
- var props_variant = yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "GetAll", new Variant ("(s)", _user_proxy.get_interface_name ()), null, DBusCallFlags.NONE, -1);
- Variant props;
- props_variant.get ("(@a{sv})", out props);
- accountsservice_props_changed_cb(_user_proxy, props, null);
- } catch (GLib.Error e) {
- debug("Unable to get properties for user %s at first try: %s", username, e.message);
- }
- }
-
- private void greeter_user_changed (string username)
- {
- setup_user_proxy.begin (username);
- }
-
- private async void setup_accountsservice ()
- {
- if (Environment.get_variable ("XDG_SESSION_CLASS") == "greeter") {
- try {
- _greeter_proxy = yield Bus.get_proxy (BusType.SESSION, "com.canonical.UnityGreeter", "/list");
- } catch (GLib.Error e) {
- warning ("unable to get greeter proxy: %s", e.message);
- return;
- }
- _greeter_proxy.entry_selected.connect (greeter_user_changed);
- yield setup_user_proxy ();
- } else {
- // We are in a user session. We just need our own proxy
- var username = Environment.get_variable ("USER");
- if (username != "" && username != null) {
- yield setup_user_proxy (username);
- }
- }
- }
-
- private async void sync_mute_to_accountsservice (bool mute)
- {
- if (_user_proxy == null)
- return;
-
- _mute_cancellable.cancel ();
- _mute_cancellable.reset ();
-
- try {
- yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "Set", new Variant ("(ssv)", _user_proxy.get_interface_name (), "Muted", new Variant ("b", mute)), null, DBusCallFlags.NONE, -1, _mute_cancellable);
- } catch (GLib.Error e) {
- warning ("unable to sync mute to AccountsService: %s", e.message);
- }
- }
-
- private async void sync_volume_to_accountsservice (double volume)
- {
- if (_user_proxy == null)
- return;
-
- _volume_cancellable.cancel ();
- _volume_cancellable.reset ();
-
- try {
- yield _user_proxy.get_connection ().call (_user_proxy.get_name (), _user_proxy.get_object_path (), "org.freedesktop.DBus.Properties", "Set", new Variant ("(ssv)", _user_proxy.get_interface_name (), "Volume", new Variant ("d", volume)), null, DBusCallFlags.NONE, -1, _volume_cancellable);
- } catch (GLib.Error e) {
- warning ("unable to sync volume to AccountsService: %s", e.message);
- }
- }
-
- private void start_local_volume_timer()
- {
- // perform a slow sync with the accounts service. max at 1 per second.
-
- // stop the AS update timer, as since we're going to be setting the volume.
- stop_account_service_volume_timer();
-
- if (_local_volume_timer == 0) {
- sync_volume_to_accountsservice.begin (_volume);
- _local_volume_timer = Timeout.add_seconds (1, local_volume_changed_timeout);
- } else {
- _send_next_local_volume = true;
- }
- }
-
- private void stop_local_volume_timer()
- {
- if (_local_volume_timer != 0) {
- Source.remove (_local_volume_timer);
- _local_volume_timer = 0;
- }
- }
-
- bool local_volume_changed_timeout()
- {
- _local_volume_timer = 0;
- if (_send_next_local_volume) {
- _send_next_local_volume = false;
- start_local_volume_timer ();
- }
- return false; // G_SOURCE_REMOVE
- }
-
- private void start_account_service_volume_timer()
- {
- if (_accountservice_volume_timer == 0) {
- // If we haven't been messing with local volume recently, apply immediately.
- if (_local_volume_timer == 0 && !set_volume_internal (_account_service_volume)) {
- return;
- }
- // Else check again in another second if needed.
- // (if AS is throwing us lots of notifications, we update at most once a second)
- _accountservice_volume_timer = Timeout.add_seconds (1, accountservice_volume_changed_timeout);
- }
- }
-
- private void stop_account_service_volume_timer()
- {
- if (_accountservice_volume_timer != 0) {
- Source.remove (_accountservice_volume_timer);
- _accountservice_volume_timer = 0;
- }
- }
-
- bool accountservice_volume_changed_timeout ()
- {
- _accountservice_volume_timer = 0;
- start_account_service_volume_timer ();
- return false; // G_SOURCE_REMOVE
- }
+ public virtual string stream { get { return ""; } }
+ public virtual bool ready { get { return false; } set { } }
+ public virtual bool active_mic { get { return false; } set { } }
+ public virtual bool high_volume { get { return false; } }
+ public virtual bool mute { get { return false; } }
+ public virtual bool is_playing { get { return false; } }
+ public virtual double volume { get { return 0.0; } set { } }
+ public virtual double mic_volume { get { return 0.0; } set { } }
+
+ public abstract void set_mute (bool mute);
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 38a76ae..6e30bf5 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -65,6 +65,14 @@ vala_add(vala-mocks
media-player-mock.vala
)
+vala_add(vala-mocks
+ media-player-list-mock.vala
+)
+
+vala_add(vala-mocks
+ volume-control-mock.vala
+)
+
vala_finish(vala-mocks
SOURCES
vala_mocks_VALA_SOURCES
@@ -184,6 +192,24 @@ target_link_libraries (
add_test(sound-menu-test sound-menu-test)
###########################
+# Notification Test
+###########################
+
+include_directories(${CMAKE_SOURCE_DIR}/src)
+add_executable (notifications-test notifications-test.cc)
+target_link_libraries (
+ notifications-test
+ indicator-sound-service-lib
+ vala-mocks-lib
+ pulse-mock
+ gtest
+ ${SOUNDSERVICE_LIBRARIES}
+ ${TEST_LIBRARIES}
+)
+
+add_test(notifications-test notifications-test)
+
+###########################
# Accounts Service User
###########################
diff --git a/tests/gtest-gvariant.h b/tests/gtest-gvariant.h
new file mode 100644
index 0000000..38fde0f
--- /dev/null
+++ b/tests/gtest-gvariant.h
@@ -0,0 +1,110 @@
+/*
+ * 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>
+ */
+
+#include <gtest/gtest.h>
+#include <gio/gio.h>
+
+namespace GTestGVariant {
+
+testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, GVariant * expect, GVariant * have)
+{
+ if (expect == nullptr && have == nullptr) {
+ auto result = testing::AssertionSuccess();
+ return result;
+ }
+
+ if (expect == nullptr || have == nullptr) {
+ gchar * havePrint;
+ if (have == nullptr) {
+ havePrint = g_strdup("(nullptr)");
+ } else {
+ havePrint = g_variant_print(have, TRUE);
+ }
+
+ auto result = testing::AssertionFailure();
+ result <<
+ " Result: " << haveStr << std::endl <<
+ " Value: " << havePrint << std::endl <<
+ " Expected: " << expectStr << std::endl;
+
+ g_free(havePrint);
+ return result;
+ }
+
+ if (g_variant_equal(expect, have)) {
+ auto result = testing::AssertionSuccess();
+ return result;
+ } else {
+ gchar * havePrint = g_variant_print(have, TRUE);
+ gchar * expectPrint = g_variant_print(expect, TRUE);
+
+ auto result = testing::AssertionFailure();
+ result <<
+ " Result: " << haveStr << std::endl <<
+ " Value: " << havePrint << std::endl <<
+ " Expected: " << expectStr << std::endl <<
+ " Expected: " << expectPrint << std::endl;
+
+ g_free(havePrint);
+ g_free(expectPrint);
+
+ return result;
+ }
+}
+
+testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, std::shared_ptr<GVariant> expect, std::shared_ptr<GVariant> have)
+{
+ return expectVariantEqual(expectStr, haveStr, expect.get(), have.get());
+}
+
+testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, const char * expect, std::shared_ptr<GVariant> have)
+{
+ auto expectv = std::shared_ptr<GVariant>([expect] {
+ auto variant = g_variant_parse(nullptr, expect, nullptr, nullptr, nullptr);
+ if (variant != nullptr)
+ g_variant_ref_sink(variant);
+ return variant;
+ }(),
+ [](GVariant * variant) {
+ if (variant != nullptr)
+ g_variant_unref(variant);
+ });
+
+ return expectVariantEqual(expectStr, haveStr, expectv, have);
+}
+
+testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, const char * expect, GVariant * have)
+{
+ auto havep = std::shared_ptr<GVariant>([have] {
+ if (have != nullptr)
+ g_variant_ref_sink(have);
+ return have;
+ }(),
+ [](GVariant * variant) {
+ if (variant != nullptr)
+ g_variant_unref(variant);
+ });
+
+ return expectVariantEqual(expectStr, haveStr, expect, havep);
+}
+
+}; // ns GTestGVariant
+
+#define EXPECT_GVARIANT_EQ(expect, have) \
+ EXPECT_PRED_FORMAT2(GTestGVariant::expectVariantEqual, expect, have)
diff --git a/tests/indicator-test.cc b/tests/indicator-test.cc
index f7d0b2b..b41a1ab 100644
--- a/tests/indicator-test.cc
+++ b/tests/indicator-test.cc
@@ -22,6 +22,7 @@
#include "indicator-fixture.h"
#include "accounts-service-mock.h"
+#include "notifications-mock.h"
class IndicatorTest : public IndicatorFixture
{
@@ -32,6 +33,7 @@ protected:
}
std::shared_ptr<AccountsServiceMock> as;
+ std::shared_ptr<NotificationsMock> notification;
virtual void SetUp() override
{
@@ -45,12 +47,16 @@ protected:
as = std::make_shared<AccountsServiceMock>();
addMock(*as);
+ notification = std::make_shared<NotificationsMock>();
+ addMock(*notification);
+
IndicatorFixture::SetUp();
}
virtual void TearDown() override
{
as.reset();
+ notification.reset();
IndicatorFixture::TearDown();
}
diff --git a/tests/media-player-list-mock.vala b/tests/media-player-list-mock.vala
new file mode 100644
index 0000000..44a6ae6
--- /dev/null
+++ b/tests/media-player-list-mock.vala
@@ -0,0 +1,25 @@
+/*
+ * Copyright © 2014 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 MediaPlayerListMock : MediaPlayerList {
+ public override MediaPlayerList.Iterator iterator () { return new MediaPlayerList.Iterator(); }
+
+ public override void sync (string[] ids) { return; }
+}
+
diff --git a/tests/notifications-mock.h b/tests/notifications-mock.h
new file mode 100644
index 0000000..b0f3b74
--- /dev/null
+++ b/tests/notifications-mock.h
@@ -0,0 +1,155 @@
+/*
+ * 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>
+ */
+
+#include <algorithm>
+#include <map>
+#include <memory>
+#include <type_traits>
+
+#include <libdbustest/dbus-test.h>
+
+class NotificationsMock
+{
+ DbusTestDbusMock * mock = nullptr;
+ 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"}) {
+ 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");
+
+ baseobj =dbus_test_dbus_mock_get_object(mock, "/org/freedesktop/Notifications", "org.freedesktop.Notifications", nullptr);
+
+ std::string capspython("ret = ");
+ capspython += vector2py(capabilities);
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "GetCapabilities", nullptr, G_VARIANT_TYPE("as"),
+ capspython.c_str(), nullptr);
+
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "GetServerInformation", nullptr, G_VARIANT_TYPE("(ssss)"),
+ "ret = ['notification-mock', 'Testing harness', '1.0', '1.1']", nullptr);
+
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "Notify", G_VARIANT_TYPE("(susssasa{sv}i)"), G_VARIANT_TYPE("u"),
+ "ret = 10", nullptr);
+
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "CloseNotification", G_VARIANT_TYPE("u"), nullptr,
+ "", nullptr);
+ }
+
+ ~NotificationsMock () {
+ g_debug("Destroying the Notifications Mock");
+ g_clear_object(&mock);
+ }
+
+ std::string vector2py (std::vector<std::string> vect) {
+ std::string retval("[ ");
+
+ std::for_each(vect.begin(), vect.end() - 1, [&retval](std::string entry) {
+ retval += "'";
+ retval += entry;
+ retval += "', ";
+ });
+
+ retval += "'";
+ retval += *(vect.end() - 1);
+ retval += "']";
+
+ return retval;
+ }
+
+ operator std::shared_ptr<DbusTestTask> () {
+ std::shared_ptr<DbusTestTask> retval(DBUS_TEST_TASK(g_object_ref(mock)), [](DbusTestTask * task) { g_clear_object(&task); });
+ return retval;
+ }
+
+ operator DbusTestTask* () {
+ return DBUS_TEST_TASK(mock);
+ }
+
+ operator DbusTestDbusMock* () {
+ return mock;
+ }
+
+ struct Notification {
+ std::string app_name;
+ unsigned int replace_id;
+ std::string app_icon;
+ std::string summary;
+ std::string body;
+ std::vector<std::string> actions;
+ std::map<std::string, std::shared_ptr<GVariant>> hints;
+ int timeout;
+ };
+
+ std::shared_ptr<GVariant> childGet (GVariant * tuple, gsize index) {
+ return std::shared_ptr<GVariant>(g_variant_get_child_value(tuple, index),
+ [](GVariant * v){ if (v != nullptr) g_variant_unref(v); });
+ }
+
+ std::vector<Notification> getNotifications (void) {
+ std::vector<Notification> notifications;
+
+ unsigned int cnt, i;
+ auto calls = dbus_test_dbus_mock_object_get_method_calls(mock, baseobj, "Notify", &cnt, nullptr);
+
+ for (i = 0; i < cnt; i++) {
+ auto call = calls[i];
+ Notification notification;
+
+ notification.app_name = g_variant_get_string(childGet(call.params, 0).get(), nullptr);
+ notification.replace_id = g_variant_get_uint32(childGet(call.params, 1).get());
+ notification.app_icon = g_variant_get_string(childGet(call.params, 2).get(), nullptr);
+ notification.summary = g_variant_get_string(childGet(call.params, 3).get(), nullptr);
+ notification.body = g_variant_get_string(childGet(call.params, 4).get(), nullptr);
+ notification.timeout = g_variant_get_int32(childGet(call.params, 7).get());
+
+ auto vactions = childGet(call.params, 5);
+ GVariantIter iactions = {0};
+ g_variant_iter_init(&iactions, vactions.get());
+ const gchar * action = nullptr;
+ while (g_variant_iter_loop(&iactions, "&s", &action)) {
+ std::string saction(action);
+ notification.actions.push_back(saction);
+ }
+
+ auto vhints = childGet(call.params, 6);
+ GVariantIter ihints = {0};
+ g_variant_iter_init(&ihints, vhints.get());
+ const gchar * hint_key = nullptr;
+ GVariant * hint_value = nullptr;
+ while (g_variant_iter_loop(&ihints, "{&sv}", &hint_key, &hint_value)) {
+ std::string key(hint_key);
+ std::shared_ptr<GVariant> value(g_variant_ref(hint_value), [](GVariant * v){ if (v != nullptr) g_variant_unref(v); });
+ notification.hints[key] = value;
+ }
+
+ notifications.push_back(notification);
+ }
+
+ return notifications;
+ }
+
+ bool clearNotifications (void) {
+ return dbus_test_dbus_mock_object_clear_method_calls(mock, baseobj, nullptr);
+ }
+};
diff --git a/tests/notifications-test.cc b/tests/notifications-test.cc
new file mode 100644
index 0000000..79b6e8e
--- /dev/null
+++ b/tests/notifications-test.cc
@@ -0,0 +1,349 @@
+/*
+ * 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>
+ */
+
+#include <memory>
+
+#include <gtest/gtest.h>
+#include <gio/gio.h>
+#include <libdbustest/dbus-test.h>
+#include <libnotify/notify.h>
+
+#include "notifications-mock.h"
+#include "gtest-gvariant.h"
+
+extern "C" {
+#include "indicator-sound-service.h"
+#include "vala-mocks.h"
+}
+
+class NotificationsTest : public ::testing::Test
+{
+ protected:
+ DbusTestService * service = NULL;
+
+ GDBusConnection * session = NULL;
+ std::shared_ptr<NotificationsMock> notifications;
+
+ virtual void SetUp() {
+ g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, TRUE);
+ g_setenv("GSETTINGS_BACKEND", "memory", TRUE);
+
+ service = dbus_test_service_new(NULL);
+ dbus_test_service_set_bus(service, DBUS_TEST_SERVICE_BUS_SESSION);
+
+ /* Useful for debugging test failures, not needed all the time (until it fails) */
+ #if 0
+ auto bustle = std::shared_ptr<DbusTestTask>([]() {
+ DbusTestTask * bustle = DBUS_TEST_TASK(dbus_test_bustle_new("notifications-test.bustle"));
+ dbus_test_task_set_name(bustle, "Bustle");
+ dbus_test_task_set_bus(bustle, DBUS_TEST_SERVICE_BUS_SESSION);
+ return bustle;
+ }(), [](DbusTestTask * bustle) {
+ g_clear_object(&bustle);
+ });
+ dbus_test_service_add_task(service, bustle.get());
+ #endif
+
+ notifications = std::make_shared<NotificationsMock>();
+
+ dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
+ dbus_test_service_start_tasks(service);
+
+ session = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
+ ASSERT_NE(nullptr, session);
+ g_dbus_connection_set_exit_on_close(session, FALSE);
+ g_object_add_weak_pointer(G_OBJECT(session), (gpointer *)&session);
+
+ /* This is done in main.c */
+ notify_init("indicator-sound");
+ }
+
+ virtual void TearDown() {
+ if (notify_is_initted())
+ notify_uninit();
+
+ notifications.reset();
+ g_clear_object(&service);
+
+ g_object_unref(session);
+
+ unsigned int cleartry = 0;
+ while (session != NULL && cleartry < 100) {
+ loop(100);
+ cleartry++;
+ }
+
+ ASSERT_EQ(nullptr, session);
+ }
+
+ static gboolean timeout_cb (gpointer user_data) {
+ GMainLoop * loop = static_cast<GMainLoop *>(user_data);
+ g_main_loop_quit(loop);
+ return G_SOURCE_REMOVE;
+ }
+
+ void loop (unsigned int ms) {
+ GMainLoop * loop = g_main_loop_new(NULL, FALSE);
+ g_timeout_add(ms, timeout_cb, loop);
+ g_main_loop_run(loop);
+ g_main_loop_unref(loop);
+ }
+
+ static int unref_idle (gpointer user_data) {
+ g_variant_unref(static_cast<GVariant *>(user_data));
+ return G_SOURCE_REMOVE;
+ }
+
+ std::shared_ptr<MediaPlayerList> playerListMock () {
+ auto playerList = std::shared_ptr<MediaPlayerList>(
+ MEDIA_PLAYER_LIST(media_player_list_mock_new()),
+ [](MediaPlayerList * list) {
+ g_clear_object(&list);
+ });
+ return playerList;
+ }
+
+ std::shared_ptr<VolumeControl> volumeControlMock () {
+ auto volumeControl = std::shared_ptr<VolumeControl>(
+ VOLUME_CONTROL(volume_control_mock_new()),
+ [](VolumeControl * control){
+ g_clear_object(&control);
+ });
+ return volumeControl;
+ }
+
+ std::shared_ptr<IndicatorSoundService> standardService (std::shared_ptr<VolumeControl> volumeControl, std::shared_ptr<MediaPlayerList> playerList) {
+ auto soundService = std::shared_ptr<IndicatorSoundService>(
+ indicator_sound_service_new(playerList.get(), volumeControl.get(), nullptr),
+ [](IndicatorSoundService * service){
+ g_clear_object(&service);
+ });
+
+ return soundService;
+ }
+};
+
+TEST_F(NotificationsTest, BasicObject) {
+ auto soundService = standardService(volumeControlMock(), playerListMock());
+
+ /* Give some time settle */
+ loop(50);
+
+ /* Auto free */
+}
+
+TEST_F(NotificationsTest, VolumeChanges) {
+ auto volumeControl = volumeControlMock();
+ auto soundService = standardService(volumeControl, playerListMock());
+
+ /* Set a volume */
+ notifications->clearNotifications();
+ volume_control_set_volume(volumeControl.get(), 0.50);
+ loop(50);
+ auto notev = notifications->getNotifications();
+ ASSERT_EQ(1, notev.size());
+ EXPECT_EQ("indicator-sound", notev[0].app_name);
+ EXPECT_EQ("Volume", notev[0].summary);
+ EXPECT_EQ(0, notev[0].actions.size());
+ EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-private-synchronous"]);
+ EXPECT_GVARIANT_EQ("@i 50", notev[0].hints["value"]);
+
+ /* Set a different volume */
+ notifications->clearNotifications();
+ volume_control_set_volume(volumeControl.get(), 0.60);
+ loop(50);
+ notev = notifications->getNotifications();
+ ASSERT_EQ(1, notev.size());
+ EXPECT_GVARIANT_EQ("@i 60", notev[0].hints["value"]);
+
+ /* Set the same volume */
+ notifications->clearNotifications();
+ volume_control_set_volume(volumeControl.get(), 0.60);
+ loop(50);
+ notev = notifications->getNotifications();
+ ASSERT_EQ(0, notev.size());
+
+ /* Change just a little */
+ notifications->clearNotifications();
+ volume_control_set_volume(volumeControl.get(), 0.60001);
+ loop(50);
+ notev = notifications->getNotifications();
+ ASSERT_EQ(0, notev.size());
+}
+
+TEST_F(NotificationsTest, StreamChanges) {
+ auto volumeControl = volumeControlMock();
+ auto soundService = standardService(volumeControl, playerListMock());
+
+ /* Set a volume */
+ notifications->clearNotifications();
+ volume_control_set_volume(volumeControl.get(), 0.5);
+ loop(50);
+ auto notev = notifications->getNotifications();
+ ASSERT_EQ(1, notev.size());
+
+ /* Change Streams, no volume change */
+ notifications->clearNotifications();
+ volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "alarm");
+ volume_control_set_volume(volumeControl.get(), 0.5);
+ loop(50);
+ notev = notifications->getNotifications();
+ EXPECT_EQ(0, notev.size());
+
+ /* Change Streams, volume change */
+ notifications->clearNotifications();
+ volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "alert");
+ volume_control_set_volume(volumeControl.get(), 0.60);
+ loop(50);
+ notev = notifications->getNotifications();
+ EXPECT_EQ(0, notev.size());
+
+ /* Change Streams, no volume change, volume up */
+ notifications->clearNotifications();
+ volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "multimedia");
+ volume_control_set_volume(volumeControl.get(), 0.60);
+ loop(50);
+ volume_control_set_volume(volumeControl.get(), 0.65);
+ notev = notifications->getNotifications();
+ EXPECT_EQ(1, notev.size());
+ EXPECT_GVARIANT_EQ("@i 65", notev[0].hints["value"]);
+}
+
+TEST_F(NotificationsTest, IconTesting) {
+ auto volumeControl = volumeControlMock();
+ auto soundService = standardService(volumeControl, playerListMock());
+
+ /* Set an initial volume */
+ notifications->clearNotifications();
+ volume_control_set_volume(volumeControl.get(), 0.5);
+ loop(50);
+ auto notev = notifications->getNotifications();
+ ASSERT_EQ(1, notev.size());
+
+ /* Generate a set of notifications */
+ notifications->clearNotifications();
+ for (float i = 0.0; i < 1.01; i += 0.1) {
+ volume_control_set_volume(volumeControl.get(), i);
+ }
+
+ loop(50);
+ notev = notifications->getNotifications();
+ ASSERT_EQ(11, notev.size());
+
+ EXPECT_EQ("audio-volume-muted", notev[0].app_icon);
+ EXPECT_EQ("audio-volume-low", notev[1].app_icon);
+ EXPECT_EQ("audio-volume-low", notev[2].app_icon);
+ EXPECT_EQ("audio-volume-medium", notev[3].app_icon);
+ EXPECT_EQ("audio-volume-medium", notev[4].app_icon);
+ EXPECT_EQ("audio-volume-medium", notev[5].app_icon);
+ EXPECT_EQ("audio-volume-medium", notev[6].app_icon);
+ EXPECT_EQ("audio-volume-high", notev[7].app_icon);
+ EXPECT_EQ("audio-volume-high", notev[8].app_icon);
+ EXPECT_EQ("audio-volume-high", notev[9].app_icon);
+ EXPECT_EQ("audio-volume-high", notev[10].app_icon);
+}
+
+TEST_F(NotificationsTest, ServerRestart) {
+ auto volumeControl = volumeControlMock();
+ auto soundService = standardService(volumeControl, playerListMock());
+
+ /* Set a volume */
+ notifications->clearNotifications();
+ volume_control_set_volume(volumeControl.get(), 0.50);
+ loop(50);
+ auto notev = notifications->getNotifications();
+ ASSERT_EQ(1, notev.size());
+
+ /* Restart server without sync notifications */
+ notifications->clearNotifications();
+ dbus_test_service_remove_task(service, (DbusTestTask*)*notifications);
+ notifications.reset();
+
+ loop(50);
+
+ notifications = std::make_shared<NotificationsMock>(std::vector<std::string>({"body", "body-markup", "icon-static"}));
+ dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
+ dbus_test_task_run((DbusTestTask*)*notifications);
+
+ /* Change the volume */
+ notifications->clearNotifications();
+ volume_control_set_volume(volumeControl.get(), 0.60);
+ loop(50);
+ notev = notifications->getNotifications();
+ ASSERT_EQ(0, notev.size());
+
+ /* Put a good server back */
+ dbus_test_service_remove_task(service, (DbusTestTask*)*notifications);
+ notifications.reset();
+
+ loop(50);
+
+ notifications = std::make_shared<NotificationsMock>();
+ dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
+ dbus_test_task_run((DbusTestTask*)*notifications);
+
+ /* Change the volume again */
+ notifications->clearNotifications();
+ volume_control_set_volume(volumeControl.get(), 0.70);
+ loop(50);
+ notev = notifications->getNotifications();
+ ASSERT_EQ(1, notev.size());
+}
+
+TEST_F(NotificationsTest, HighVolume) {
+ auto volumeControl = volumeControlMock();
+ auto soundService = standardService(volumeControl, playerListMock());
+
+ /* Set a volume */
+ notifications->clearNotifications();
+ volume_control_set_volume(volumeControl.get(), 0.50);
+ loop(50);
+ auto notev = notifications->getNotifications();
+ ASSERT_EQ(1, notev.size());
+ EXPECT_EQ("Volume", notev[0].summary);
+ EXPECT_EQ("", notev[0].body);
+ EXPECT_GVARIANT_EQ("@s 'false'", notev[0].hints["x-canonical-value-bar-tint"]);
+
+ /* Set high volume with volume change */
+ notifications->clearNotifications();
+ volume_control_mock_set_mock_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), TRUE);
+ volume_control_set_volume(volumeControl.get(), 0.90);
+ loop(50);
+ notev = notifications->getNotifications();
+ ASSERT_LT(0, notev.size()); /* This passes with one or two since it would just be an update to the first if a second was sent */
+ EXPECT_EQ("Volume", notev[0].summary);
+ EXPECT_EQ("High volume", notev[0].body);
+ EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-value-bar-tint"]);
+
+ /* Move it back */
+ volume_control_mock_set_mock_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), FALSE);
+ volume_control_set_volume(volumeControl.get(), 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_mock_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), TRUE);
+ loop(50);
+ notev = notifications->getNotifications();
+ ASSERT_EQ(1, notev.size());
+ EXPECT_EQ("Volume", notev[0].summary);
+ EXPECT_EQ("High volume", notev[0].body);
+ EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-value-bar-tint"]);
+}
diff --git a/tests/volume-control-mock.vala b/tests/volume-control-mock.vala
new file mode 100644
index 0000000..4b846bf
--- /dev/null
+++ b/tests/volume-control-mock.vala
@@ -0,0 +1,47 @@
+/*
+ * -*- 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 VolumeControlMock : VolumeControl
+{
+ public string mock_stream { get; set; default = "multimedia"; }
+ public override string stream { get { return mock_stream; } }
+ public override bool ready { get; set; }
+ public override bool active_mic { get; set; }
+ public bool mock_high_volume { get; set; }
+ public override bool high_volume { get { return mock_high_volume; } }
+ 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; } }
+ public override double volume { get; set; }
+ public override double mic_volume { get; set; }
+
+ public override void set_mute (bool mute) {
+
+ }
+
+ public VolumeControlMock() {
+ ready = true;
+ this.notify["mock-stream"].connect(() => this.notify_property("stream"));
+ this.notify["mock-high-volume"].connect(() => this.notify_property("high-volume"));
+ this.notify["mock-mute"].connect(() => this.notify_property("mute"));
+ this.notify["mock-is-playing"].connect(() => this.notify_property("is-playing"));
+ }
+}
diff --git a/tests/volume-control-test.cc b/tests/volume-control-test.cc
index 9970241..41e1886 100644
--- a/tests/volume-control-test.cc
+++ b/tests/volume-control-test.cc
@@ -71,13 +71,13 @@ class VolumeControlTest : public ::testing::Test
};
TEST_F(VolumeControlTest, BasicObject) {
- VolumeControl * control = volume_control_new();
+ VolumeControlPulse * control = volume_control_pulse_new();
/* Setup the PA backend */
loop(100);
/* Ready */
- EXPECT_TRUE(volume_control_get_ready(control));
+ EXPECT_TRUE(volume_control_get_ready(VOLUME_CONTROL(control)));
g_clear_object(&control);
}