diff -Nru indicator-sound-12.10.2+15.04.20150205/debian/changelog indicator-sound-12.10.2+15.04.20150219.1/debian/changelog --- indicator-sound-12.10.2+15.04.20150205/debian/changelog 2015-02-19 22:36:15.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/debian/changelog 2015-02-19 22:36:15.000000000 +0000 @@ -1,3 +1,13 @@ +indicator-sound (12.10.2+15.04.20150219.1-0ubuntu1) vivid; urgency=medium + + [ Ted Gould ] + * Add eventually functions to indicator fixture + * Add notifications mock and tests + * Track the reason a volume was set to filter volume changes more + accurately (LP: #1416520) + + -- CI Train Bot Thu, 19 Feb 2015 16:27:48 +0000 + indicator-sound (12.10.2+15.04.20150205-0ubuntu1) vivid; urgency=medium [ Ted Gould ] diff -Nru indicator-sound-12.10.2+15.04.20150205/src/CMakeLists.txt indicator-sound-12.10.2+15.04.20150219.1/src/CMakeLists.txt --- indicator-sound-12.10.2+15.04.20150205/src/CMakeLists.txt 2015-02-05 17:18:57.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/src/CMakeLists.txt 2015-02-19 16:27:42.000000000 +0000 @@ -43,6 +43,7 @@ DEPENDS sound-menu volume-control + volume-control-pulse media-player media-player-list mpris2-interfaces @@ -52,6 +53,11 @@ 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 @@ -154,6 +160,7 @@ add_definitions( -w + -DG_LOG_DOMAIN="indicator-sound" ) add_library( @@ -164,6 +171,7 @@ target_link_libraries( indicator-sound-service-lib ${SOUNDSERVICE_LIBRARIES} + -lm ) ########################### diff -Nru indicator-sound-12.10.2+15.04.20150205/src/main.c indicator-sound-12.10.2+15.04.20150219.1/src/main.c --- indicator-sound-12.10.2+15.04.20150205/src/main.c 2015-02-05 17:18:57.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/src/main.c 2015-02-19 16:27:24.000000000 +0000 @@ -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 . + */ #include #include @@ -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 -Nru indicator-sound-12.10.2+15.04.20150205/src/media-player-list.vala indicator-sound-12.10.2+15.04.20150219.1/src/media-player-list.vala --- indicator-sound-12.10.2+15.04.20150205/src/media-player-list.vala 2015-02-05 17:18:57.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/src/media-player-list.vala 2015-02-19 16:27:24.000000000 +0000 @@ -17,7 +17,7 @@ * Ted Gould */ -public class MediaPlayerList { +public abstract class MediaPlayerList : Object { public class Iterator { public virtual MediaPlayer? next_value() { return null; diff -Nru indicator-sound-12.10.2+15.04.20150205/src/service.vala indicator-sound-12.10.2+15.04.20150219.1/src/service.vala --- indicator-sound-12.10.2+15.04.20150205/src/service.vala 2015-02-05 17:19:12.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/src/service.vala 2015-02-19 16:27:42.000000000 +0000 @@ -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 @@ 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 @@ } } }); + + /* 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 @@ 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 { @@ -159,7 +168,7 @@ } /* Normalize volume, because the volume action's state is [0.0, 1.0], see create_volume_action() */ - this.actions.change_action_state ("volume", this.volume_control.volume / this.max_volume); + this.actions.change_action_state ("volume", this.volume_control.volume.volume / this.max_volume); } } @@ -171,7 +180,6 @@ { "indicator-shown", null, null, "@b false", null }, }; - MainLoop loop; SimpleActionGroup actions; HashTable menus; Settings settings; @@ -185,7 +193,6 @@ 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(). @@ -197,8 +204,11 @@ void activate_scroll_action (SimpleAction action, Variant? param) { int delta = param.get_int32(); /* positive for up, negative for down */ - double v = this.volume_control.volume + volume_step_percentage * delta; - this.volume_control.volume = v.clamp (0.0, this.max_volume); + var scrollvol = new VolumeControl.Volume(); + double v = this.volume_control.volume.volume + volume_step_percentage * delta; + scrollvol.volume = v.clamp (0.0, this.max_volume); + scrollvol.reason = VolumeControl.VolumeReasons.USER_KEYPRESS; + this.volume_control.volume = scrollvol; } void activate_desktop_settings (SimpleAction action, Variant? param) { @@ -233,7 +243,7 @@ } void update_root_icon () { - double volume = this.volume_control.volume; + double volume = this.volume_control.volume.volume; string icon; if (this.volume_control.mute) icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; @@ -270,11 +280,10 @@ private bool check_sync_notification = false; private bool support_sync_notification = false; - private string last_output_notification = "multimedia"; - private double last_volume_notification = 0; void update_sync_notification () { if (!check_sync_notification) { + support_sync_notification = false; List caps = Notify.get_server_caps (); if (caps.find_custom ("x-canonical-private-synchronous", strcmp) != null) { support_sync_notification = true; @@ -285,21 +294,6 @@ 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; @@ -311,11 +305,11 @@ /* Choose an icon */ string icon = "audio-volume-muted"; - if (volume_control.volume <= 0.0) + if (volume_control.volume.volume <= 0.0) icon = "audio-volume-muted"; - else if (volume_control.volume <= 0.3) + else if (volume_control.volume.volume <= 0.3) icon = "audio-volume-low"; - else if (volume_control.volume <= 0.7) + else if (volume_control.volume.volume <= 0.7) icon = "audio-volume-medium"; else icon = "audio-volume-high"; @@ -328,7 +322,7 @@ /* Put it all into the notification */ sync_notification.clear_hints (); sync_notification.update (_("Volume"), volume_label, icon); - sync_notification.set_hint ("value", (int32)(volume_control.volume * 100.0)); + sync_notification.set_hint ("value", (int32)Math.round(volume_control.volume.volume * 100.0)); sync_notification.set_hint ("x-canonical-value-bar-tint", tint); sync_notification.set_hint ("x-canonical-private-synchronous", "true"); sync_notification.set_hint ("x-canonical-non-shaped-icon", "true"); @@ -341,13 +335,14 @@ } } + 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 +366,9 @@ 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 +411,7 @@ 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 @@ -423,30 +420,40 @@ * volume_control.set_volume(). */ - double volume = this.volume_control.volume / this.max_volume; + double volume = this.volume_control.volume.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; - volume_control.volume = v.clamp (0.0, this.max_volume); + + var vol = new VolumeControl.Volume(); + vol.volume = v.clamp (0.0, this.max_volume); + vol.reason = VolumeControl.VolumeReasons.USER_KEYPRESS; + volume_control.volume = vol; }); /* activating this action changes the volume by the amount given in the parameter */ volume_action.activate.connect ( (action, param) => { int delta = param.get_int32 (); - double v = volume_control.volume + volume_step_percentage * delta; - volume_control.volume = v.clamp (0.0, this.max_volume); + double v = volume_control.volume.volume + volume_step_percentage * delta; + + var vol = new VolumeControl.Volume(); + vol.volume = v.clamp (0.0, this.max_volume); + vol.reason = VolumeControl.VolumeReasons.USER_KEYPRESS; + volume_control.volume = vol; }); 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.volume / this.max_volume)); this.update_root_icon (); - this.update_sync_notification (); + + var reason = volume_control.volume.reason; + if (reason == VolumeControl.VolumeReasons.USER_KEYPRESS || + reason == VolumeControl.VolumeReasons.DEVICE_OUTPUT_CHANGE) + this.update_sync_notification (); }); this.volume_control.bind_property ("ready", volume_action, "enabled", BindingFlags.SYNC_CREATE); @@ -454,24 +461,26 @@ 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 +490,7 @@ 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 -Nru indicator-sound-12.10.2+15.04.20150205/src/sound-menu.vala indicator-sound-12.10.2+15.04.20150219.1/src/sound-menu.vala --- indicator-sound-12.10.2+15.04.20150205/src/sound-menu.vala 2015-02-05 17:18:57.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/src/sound-menu.vala 2015-02-19 16:27:24.000000000 +0000 @@ -73,9 +73,20 @@ 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 -Nru indicator-sound-12.10.2+15.04.20150205/src/volume-control-pulse.vala indicator-sound-12.10.2+15.04.20150219.1/src/volume-control-pulse.vala --- indicator-sound-12.10.2+15.04.20150205/src/volume-control-pulse.vala 1970-01-01 00:00:00.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/src/volume-control-pulse.vala 2015-02-19 16:27:42.000000000 +0000 @@ -0,0 +1,911 @@ +/* + * -*- 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 . + * + * Authors: + * Alberto Ruiz + */ + +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 VolumeControl.Volume _volume = new VolumeControl.Volume(); + private double _mic_volume = 0.0; + + /* Used by the pulseaudio stream restore extension */ + private DBusConnection _pconn; + /* Need both the list and hash so we can retrieve the last known sink-input after + * releasing the current active one (restoring back to the previous known role) */ + private Gee.ArrayList _sink_input_list = new Gee.ArrayList (); + private HashMap _sink_input_hash = new HashMap (); + private bool _pulse_use_stream_restore = false; + private int32 _active_sink_input = -1; + private string[] _valid_roles = {"multimedia", "alert", "alarm", "phone"}; + public override string stream { + get { + if (_active_sink_input < 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.volume > 0.75 && _active_port_headphone; + } + } + + public VolumeControlPulse () + { + _volume.volume = 0.0; + _volume.reason = VolumeControl.VolumeReasons.PULSE_CHANGE; + + 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) + { + 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 != volume_to_double (i.volume.max ())) + { + var vol = new VolumeControl.Volume(); + vol.volume = volume_to_double (i.volume.max ()); + vol.reason = VolumeControl.VolumeReasons.PULSE_CHANGE; + this.volume = vol; + } + } + + private void source_info_cb (Context c, SourceInfo? i, int eol) + { + if (i == null) + return; + + if (_mic_volume != volume_to_double (i.volume.values[0])) + { + _mic_volume = volume_to_double (i.volume.values[0]); + this.notify_property ("mic-volume"); + } + } + + private void server_info_cb_for_props (Context c, ServerInfo? i) + { + if (i == null) + return; + context.get_sink_info_by_name (i.default_sink_name, sink_info_cb_for_props); + } + + private void update_sink () + { + context.get_server_info (server_info_cb_for_props); + } + + private void update_source_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) { + if (i != null) + context.get_source_info_by_name (i.default_source_name, source_info_cb); + } + + private void update_source () + { + context.get_server_info (update_source_get_server_info_cb); + } + + private DBusMessage pulse_dbus_filter (DBusConnection connection, owned DBusMessage message, bool incoming) + { + if (message.get_message_type () == DBusMessageType.SIGNAL) { + string active_role_objp = _objp_role_alert; + if (_active_sink_input != -1) + active_role_objp = _sink_input_hash.get (_active_sink_input); + + if (message.get_path () == active_role_objp && message.get_member () == "VolumeUpdated") { + uint sig_count = 0; + lock (_pa_volume_sig_count) { + sig_count = _pa_volume_sig_count; + if (_pa_volume_sig_count > 0) + _pa_volume_sig_count--; + } + + /* We only care about signals if our internal count is zero */ + if (sig_count == 0) { + /* Extract volume and make sure it's not a side effect of us setting it */ + Variant body = message.get_body (); + Variant varray = body.get_child_value (0); + + uint32 type = 0, lvolume = 0; + VariantIter iter = varray.iterator (); + iter.next ("(uu)", &type, &lvolume); + /* Here we need to compare integer values to avoid rounding issues, so just + * using the volume values used by pulseaudio */ + PulseAudio.Volume cvolume = double_to_volume (_volume.volume); + if (lvolume != cvolume) { + /* Someone else changed the volume for this role, reflect on the indicator */ + var vol = new VolumeControl.Volume(); + vol.volume = volume_to_double (lvolume); + vol.reason = VolumeControl.VolumeReasons.PULSE_CHANGE; + this.volume = vol; + } + } + } + } + + return message; + } + + private async void update_active_sink_input (int32 index) + { + if ((index == -1) || (index != _active_sink_input && index in _sink_input_list)) { + string sink_input_objp = _objp_role_alert; + if (index != -1) + sink_input_objp = _sink_input_hash.get (index); + _active_sink_input = index; + + /* 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); + + var vol = new VolumeControl.Volume(); + vol.volume = volume_to_double (volume); + vol.reason = VolumeControl.VolumeReasons.VOLUME_STREAM_CHANGE; + this.volume = vol; + } catch (GLib.Error e) { + warning ("unable to get volume for active role %s (%s)", sink_input_objp, e.message); + } + } + } + + private void add_sink_input_into_list (SinkInputInfo sink_input) + { + /* We're only adding ones that are not corked and with a valid role */ + 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 ((int32)sink_input.index); + } + } + } + + private void remove_sink_input_from_list (uint32 index) + { + if (index in _sink_input_list) { + _sink_input_list.remove (index); + _sink_input_hash.unset (index); + if (index == _active_sink_input) { + if (_sink_input_list.size != 0) + update_active_sink_input.begin ((int32)_sink_input_list.get (0)); + else + update_active_sink_input.begin (-1); + } + } + } + + private void handle_new_sink_input_cb (Context c, SinkInputInfo? i, int eol) + { + if (i == null) + return; + + add_sink_input_into_list (i); + } + + private void handle_changed_sink_input_cb (Context c, SinkInputInfo? i, int eol) + { + if (i == null) + return; + + if (i.index in _sink_input_list) { + /* Phone stream is always corked, so handle it differently */ + if (i.corked == 1 && _sink_input_hash.get (i.index) != _objp_role_phone) + remove_sink_input_from_list (i.index); + } else { + if (i.corked == 0) + add_sink_input_into_list (i); + } + } + + private void source_output_info_cb (Context c, SourceOutputInfo? i, int eol) + { + if (i == null) + return; + + 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.volume)); + c.set_sink_volume_by_index (i.index, cvol, set_volume_success_cb); + } + + private void server_info_cb_for_set_volume (Context c, ServerInfo? i) + { + if (i == null) + { + warning ("Could not get PulseAudio server info"); + return; + } + + context.get_sink_info_by_name (i.default_sink_name, sink_info_set_volume_cb); + } + + private async void set_volume_active_role () + { + string active_role_objp = _objp_role_alert; + + if (_active_sink_input != -1 && _active_sink_input in _sink_input_list) + active_role_objp = _sink_input_hash.get (_active_sink_input); + + try { + double vol = _volume.volume; + var builder = new VariantBuilder (new VariantType ("a(uu)")); + builder.add ("(uu)", 0, double_to_volume (vol)); + Variant volume = builder.end (); + + /* Increase the signal counter so we can handle the callback */ + lock (_pa_volume_sig_count) { + _pa_volume_sig_count++; + } + + yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry", + active_role_objp, "org.freedesktop.DBus.Properties", "Set", + new Variant ("(ssv)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume", volume), + null, DBusCallFlags.NONE, -1); + + debug ("Set volume to %f on path %s", vol, active_role_objp); + } catch (GLib.Error e) { + lock (_pa_volume_sig_count) { + _pa_volume_sig_count--; + } + warning ("unable to set volume for stream obj path %s (%s)", active_role_objp, e.message); + } + } + + void set_mic_volume_success_cb (Context c, int success) + { + if ((bool)success) + this.notify_property ("mic-volume"); + } + + void set_mic_volume_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) { + if (i != null) { + unowned CVolume cvol = CVolume (); + cvol = 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 VolumeControl.Volume volume { + get { + return _volume; + } + set { + debug("Setting volume to %f for profile %d because %d", value.volume, _active_sink_input, value.reason); + + var old_high_volume = this.high_volume; + _volume = value; + + /* Make sure we're connected to Pulse and pulse didn't give us the change */ + if (context.get_state () == Context.State.READY && + _volume.reason != VolumeControl.VolumeReasons.PULSE_CHANGE) + if (_pulse_use_stream_restore) + set_volume_active_role.begin (); + else + context.get_server_info (server_info_cb_for_set_volume); + + if (this.high_volume != old_high_volume) + this.notify_property("high-volume"); + + 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; + } + } + + debug ("PulseAudio dbus unix socket: %s", 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) { + debug ("Using PulseAudio DBUS Stream Restore module"); + /* 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); + debug ("Found obj path %s for restore data named %s\n", objp, name); + } catch (GLib.Error e) { + warning ("unable to find stream restore data for: %s", 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 (VolumeControl.Volume 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.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) { + var vol = new VolumeControl.Volume(); + vol.volume = _account_service_volume; + vol.reason = VolumeControl.VolumeReasons.ACCOUNTS_SERVICE_SET; + this.volume = vol; + return; + } + // Else check again in another second if needed. + // (if AS is throwing us lots of notifications, we update at most once a second) + _accountservice_volume_timer = Timeout.add_seconds (1, accountservice_volume_changed_timeout); + } + } + + private void stop_account_service_volume_timer() + { + if (_accountservice_volume_timer != 0) { + Source.remove (_accountservice_volume_timer); + _accountservice_volume_timer = 0; + } + } + + bool accountservice_volume_changed_timeout () + { + _accountservice_volume_timer = 0; + start_account_service_volume_timer (); + return false; // G_SOURCE_REMOVE + } +} diff -Nru indicator-sound-12.10.2+15.04.20150205/src/volume-control.vala indicator-sound-12.10.2+15.04.20150219.1/src/volume-control.vala --- indicator-sound-12.10.2+15.04.20150205/src/volume-control.vala 2015-02-05 17:19:12.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/src/volume-control.vala 2015-02-19 16:27:42.000000000 +0000 @@ -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,33 @@ * along with this program. If not, see . * * Authors: - * Alberto Ruiz + * Ted Gould */ -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 _sink_input_list = new Gee.ArrayList (); - private HashMap _sink_input_hash = new HashMap (); - 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; + public enum VolumeReasons { + PULSE_CHANGE, + ACCOUNTS_SERVICE_SET, + DEVICE_OUTPUT_CHANGE, + USER_KEYPRESS, + VOLUME_STREAM_CHANGE + } + + public class Volume : Object { + public double volume; + public VolumeReasons reason; + } + + 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; } } + private Volume _volume; + public virtual Volume volume { get { return _volume; } set { } } + public virtual double mic_volume { get { return 0.0; } set { } } - /* 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 abstract void set_mute (bool mute); } diff -Nru indicator-sound-12.10.2+15.04.20150205/tests/CMakeLists.txt indicator-sound-12.10.2+15.04.20150219.1/tests/CMakeLists.txt --- indicator-sound-12.10.2+15.04.20150205/tests/CMakeLists.txt 2015-02-05 17:19:33.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/tests/CMakeLists.txt 2015-02-19 16:27:24.000000000 +0000 @@ -65,6 +65,14 @@ 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 @@ 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 -Nru indicator-sound-12.10.2+15.04.20150205/tests/gtest-gvariant.h indicator-sound-12.10.2+15.04.20150219.1/tests/gtest-gvariant.h --- indicator-sound-12.10.2+15.04.20150205/tests/gtest-gvariant.h 1970-01-01 00:00:00.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/tests/gtest-gvariant.h 2015-02-19 16:27:24.000000000 +0000 @@ -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 . + * + * Authors: + * Ted Gould + */ + +#include +#include + +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 expect, std::shared_ptr have) +{ + return expectVariantEqual(expectStr, haveStr, expect.get(), have.get()); +} + +testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, const char * expect, std::shared_ptr have) +{ + auto expectv = std::shared_ptr([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([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 -Nru indicator-sound-12.10.2+15.04.20150205/tests/indicator-fixture.h indicator-sound-12.10.2+15.04.20150219.1/tests/indicator-fixture.h --- indicator-sound-12.10.2+15.04.20150205/tests/indicator-fixture.h 2015-02-05 17:19:33.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/tests/indicator-fixture.h 2015-02-19 16:27:11.000000000 +0000 @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include @@ -31,7 +33,10 @@ std::string _indicatorPath; std::string _indicatorAddress; std::vector> _mocks; + protected: + std::chrono::milliseconds _eventuallyTime; + private: class PerRunData { public: /* We're private in the fixture but other than that we don't care, @@ -116,6 +121,7 @@ const std::string& addr) : _indicatorPath(path) , _indicatorAddress(addr) + , _eventuallyTime(std::chrono::seconds(5)) { }; @@ -195,6 +201,38 @@ waitForCore(G_OBJECT(group.get()), "action-added"); } + testing::AssertionResult expectEventually (std::function &testfunc) { + auto loop = std::shared_ptr(g_main_loop_new(nullptr, FALSE), [](GMainLoop * loop) { if (loop != nullptr) g_main_loop_unref(loop); }); + + std::promise retpromise; + auto retfuture = retpromise.get_future(); + auto start = std::chrono::steady_clock::now(); + + /* The core of the idle function as an object so we can use the C++-isms + of attaching the variables and make this code reasonably readable */ + std::function idlefunc = [&loop, &retpromise, &testfunc, &start, this]() -> void { + auto result = testfunc(); + + if (result == false && _eventuallyTime > (std::chrono::steady_clock::now() - start)) { + return; + } + + retpromise.set_value(result); + g_main_loop_quit(loop.get()); + }; + + auto idlesrc = g_idle_add([](gpointer data) -> gboolean { + auto func = reinterpret_cast *>(data); + (*func)(); + return G_SOURCE_CONTINUE; + }, &idlefunc); + + g_main_loop_run(loop.get()); + g_source_remove(idlesrc); + + return retfuture.get(); + } + protected: void setMenu (const std::string& path) { run->_menu.reset(); @@ -234,6 +272,13 @@ return result; } + template testing::AssertionResult expectEventuallyActionStateExists (Args&& ... args) { + std::function func = [&]() { + return expectActionStateExists(std::forward(args)...); + }; + return expectEventually(func); + } + testing::AssertionResult expectActionStateType (const char * nameStr, const char * typeStr, const std::string& name, const GVariantType * type) { auto atype = g_action_group_get_action_state_type(run->_actions.get(), name.c_str()); bool same = false; @@ -256,6 +301,13 @@ return result; } + template testing::AssertionResult expectEventuallyActionStateType (Args&& ... args) { + std::function func = [&]() { + return expectActionStateType(std::forward(args)...); + }; + return expectEventually(func); + } + testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, GVariant * value) { auto varref = std::shared_ptr(g_variant_ref_sink(value), [](GVariant * varptr) { if (varptr != nullptr) @@ -320,6 +372,13 @@ return expectActionStateIs(nameStr, valueStr, name, var); } + template testing::AssertionResult expectEventuallyActionStateIs (Args&& ... args) { + std::function func = [&]() { + return expectActionStateIs(std::forward(args)...); + }; + return expectEventually(func); + } + private: std::shared_ptr getMenuAttributeVal (int location, std::shared_ptr& menu, const std::string& attribute, std::shared_ptr& value) { @@ -363,7 +422,7 @@ } protected: - testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const gchar * attributeStr, const char * valueStr, const std::vector menuLocation, const std::string& attribute, GVariant * value) { + testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector menuLocation, const std::string& attribute, GVariant * value) { auto varref = std::shared_ptr(g_variant_ref_sink(value), [](GVariant * varptr) { if (varptr != nullptr) g_variant_unref(varptr); @@ -401,44 +460,65 @@ } } - testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const gchar * attributeStr, const char * valueStr, const std::vector menuLocation, const std::string& attribute, bool value) { + testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector menuLocation, const std::string& attribute, bool value) { GVariant * var = g_variant_new_boolean(value); return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var); } - testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const gchar * attributeStr, const char * valueStr, const std::vector menuLocation, const std::string& attribute, std::string value) { + testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector menuLocation, const std::string& attribute, std::string value) { GVariant * var = g_variant_new_string(value.c_str()); return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var); } - testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const gchar * attributeStr, const char * valueStr, const std::vector menuLocation, const std::string& attribute, const char * value) { + testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector menuLocation, const std::string& attribute, const char * value) { GVariant * var = g_variant_new_string(value); return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var); } + template testing::AssertionResult expectEventuallyMenuAttribute (Args&& ... args) { + std::function func = [&]() { + return expectMenuAttribute(std::forward(args)...); + }; + return expectEventually(func); + } }; +/* Menu Attrib */ +#define ASSERT_MENU_ATTRIB(menu, attrib, value) \ + ASSERT_PRED_FORMAT3(IndicatorFixture::expectMenuAttribute, menu, attrib, value) + #define EXPECT_MENU_ATTRIB(menu, attrib, value) \ EXPECT_PRED_FORMAT3(IndicatorFixture::expectMenuAttribute, menu, attrib, value) -#define ASSERT_MENU_ATTRIB(menu, attrib, value) \ - ASSERT_PRED_FORMAT3(IndicatorFixture::expectMenuAttribute, menu, attrib, value) +#define EXPECT_EVENTUALLY_MENU_ATTRIB(menu, attrib, value) \ + EXPECT_PRED_FORMAT3(IndicatorFixture::expectEventuallyMenuAttribute, menu, attrib, value) +/* Action Exists */ #define ASSERT_ACTION_EXISTS(action) \ ASSERT_PRED_FORMAT1(IndicatorFixture::expectActionExists, action) #define EXPECT_ACTION_EXISTS(action) \ EXPECT_PRED_FORMAT1(IndicatorFixture::expectActionExists, action) -#define EXPECT_ACTION_STATE(action, value) \ - EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionStateIs, action, value) +#define EXPECT_EVENTUALLY_ACTION_EXISTS(action) \ + EXPECT_PRED_FORMAT1(IndicatorFixture::expectEventuallyActionExists, action) +/* Action State */ #define ASSERT_ACTION_STATE(action, value) \ ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionStateIs, action, value) -#define EXPECT_ACTION_STATE_TYPE(action, type) \ - EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionStateType, action, type) +#define EXPECT_ACTION_STATE(action, value) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionStateIs, action, value) +#define EXPECT_EVENTUALLY_ACTION_STATE(action, value) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyActionStateIs, action, value) + +/* Action State Type */ #define ASSERT_ACTION_STATE_TYPE(action, type) \ ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionStateType, action, type) +#define EXPECT_ACTION_STATE_TYPE(action, type) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionStateType, action, type) + +#define EXPECT_EVENTUALLY_ACTION_STATE_TYPE(action, type) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyActionStateType, action, type) diff -Nru indicator-sound-12.10.2+15.04.20150205/tests/indicator-test.cc indicator-sound-12.10.2+15.04.20150219.1/tests/indicator-test.cc --- indicator-sound-12.10.2+15.04.20150205/tests/indicator-test.cc 2015-02-05 17:19:33.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/tests/indicator-test.cc 2015-02-19 16:27:24.000000000 +0000 @@ -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 @@ } std::shared_ptr as; + std::shared_ptr notification; virtual void SetUp() override { @@ -45,12 +47,16 @@ as = std::make_shared(); addMock(*as); + notification = std::make_shared(); + addMock(*notification); + IndicatorFixture::SetUp(); } virtual void TearDown() override { as.reset(); + notification.reset(); IndicatorFixture::TearDown(); } @@ -61,7 +67,7 @@ TEST_F(IndicatorTest, PhoneMenu) { setMenu("/com/canonical/indicator/sound/phone"); - EXPECT_MENU_ATTRIB({0}, "action", "indicator.root"); + EXPECT_EVENTUALLY_MENU_ATTRIB(std::vector({0}), "action", "indicator.root"); EXPECT_MENU_ATTRIB({0}, "x-canonical-type", "com.canonical.indicator.root"); EXPECT_MENU_ATTRIB({0}, "x-canonical-scroll-action", "indicator.scroll"); EXPECT_MENU_ATTRIB({0}, "x-canonical-secondary-action", "indicator.mute"); diff -Nru indicator-sound-12.10.2+15.04.20150205/tests/media-player-list-mock.vala indicator-sound-12.10.2+15.04.20150219.1/tests/media-player-list-mock.vala --- indicator-sound-12.10.2+15.04.20150205/tests/media-player-list-mock.vala 1970-01-01 00:00:00.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/tests/media-player-list-mock.vala 2015-02-19 16:27:24.000000000 +0000 @@ -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 . + * + * Authors: + * Ted Gould + */ + +public class MediaPlayerListMock : MediaPlayerList { + public override MediaPlayerList.Iterator iterator () { return new MediaPlayerList.Iterator(); } + + public override void sync (string[] ids) { return; } +} + diff -Nru indicator-sound-12.10.2+15.04.20150205/tests/notifications-mock.h indicator-sound-12.10.2+15.04.20150219.1/tests/notifications-mock.h --- indicator-sound-12.10.2+15.04.20150205/tests/notifications-mock.h 1970-01-01 00:00:00.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/tests/notifications-mock.h 2015-02-19 16:27:24.000000000 +0000 @@ -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 . + * + * Authors: + * Ted Gould + */ + +#include +#include +#include +#include + +#include + +class NotificationsMock +{ + DbusTestDbusMock * mock = nullptr; + DbusTestDbusMockObject * baseobj = nullptr; + + public: + NotificationsMock (std::vector 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 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 () { + std::shared_ptr 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 actions; + std::map> hints; + int timeout; + }; + + std::shared_ptr childGet (GVariant * tuple, gsize index) { + return std::shared_ptr(g_variant_get_child_value(tuple, index), + [](GVariant * v){ if (v != nullptr) g_variant_unref(v); }); + } + + std::vector getNotifications (void) { + std::vector 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 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 -Nru indicator-sound-12.10.2+15.04.20150205/tests/notifications-test.cc indicator-sound-12.10.2+15.04.20150219.1/tests/notifications-test.cc --- indicator-sound-12.10.2+15.04.20150205/tests/notifications-test.cc 1970-01-01 00:00:00.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/tests/notifications-test.cc 2015-02-19 16:27:42.000000000 +0000 @@ -0,0 +1,358 @@ +/* + * 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 . + * + * Authors: + * Ted Gould + */ + +#include + +#include +#include +#include +#include + +#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 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 * 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(); + + 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(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(user_data)); + return G_SOURCE_REMOVE; + } + + std::shared_ptr playerListMock () { + auto playerList = std::shared_ptr( + MEDIA_PLAYER_LIST(media_player_list_mock_new()), + [](MediaPlayerList * list) { + g_clear_object(&list); + }); + return playerList; + } + + std::shared_ptr volumeControlMock () { + auto volumeControl = std::shared_ptr( + VOLUME_CONTROL(volume_control_mock_new()), + [](VolumeControl * control){ + g_clear_object(&control); + }); + return volumeControl; + } + + std::shared_ptr standardService (std::shared_ptr volumeControl, std::shared_ptr playerList) { + auto soundService = std::shared_ptr( + indicator_sound_service_new(playerList.get(), volumeControl.get(), nullptr), + [](IndicatorSoundService * service){ + g_clear_object(&service); + }); + + return soundService; + } + + void setMockVolume (std::shared_ptr volumeControl, double volume, VolumeControlVolumeReasons reason = VOLUME_CONTROL_VOLUME_REASONS_USER_KEYPRESS) { + VolumeControlVolume * vol = volume_control_volume_new(); + vol->volume = volume; + vol->reason = reason; + + volume_control_set_volume(volumeControl.get(), vol); + g_object_unref(vol); + } +}; + +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(); + setMockVolume(volumeControl, 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(); + setMockVolume(volumeControl, 0.60); + loop(50); + notev = notifications->getNotifications(); + ASSERT_EQ(1, notev.size()); + EXPECT_GVARIANT_EQ("@i 60", notev[0].hints["value"]); + + /* Have pulse set a volume */ + notifications->clearNotifications(); + setMockVolume(volumeControl, 0.70, VOLUME_CONTROL_VOLUME_REASONS_PULSE_CHANGE); + loop(50); + notev = notifications->getNotifications(); + ASSERT_EQ(0, notev.size()); + + /* Have AS set the volume */ + notifications->clearNotifications(); + setMockVolume(volumeControl, 0.80, VOLUME_CONTROL_VOLUME_REASONS_ACCOUNTS_SERVICE_SET); + 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(); + setMockVolume(volumeControl, 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"); + setMockVolume(volumeControl, 0.5, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE); + 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"); + setMockVolume(volumeControl, 0.6, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE); + 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"); + setMockVolume(volumeControl, 0.6, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE); + loop(50); + setMockVolume(volumeControl, 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(); + setMockVolume(volumeControl, 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) { + setMockVolume(volumeControl, 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(); + setMockVolume(volumeControl, 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(std::vector({"body", "body-markup", "icon-static"})); + dbus_test_service_add_task(service, (DbusTestTask*)*notifications); + dbus_test_task_run((DbusTestTask*)*notifications); + + /* Change the volume */ + notifications->clearNotifications(); + setMockVolume(volumeControl, 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(); + dbus_test_service_add_task(service, (DbusTestTask*)*notifications); + dbus_test_task_run((DbusTestTask*)*notifications); + + /* Change the volume again */ + notifications->clearNotifications(); + setMockVolume(volumeControl, 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(); + setMockVolume(volumeControl, 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); + setMockVolume(volumeControl, 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); + setMockVolume(volumeControl, 0.50); + loop(50); + + /* Set high volume without level change */ + /* NOTE: This can happen if headphones are plugged in */ + notifications->clearNotifications(); + volume_control_mock_set_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 -Nru indicator-sound-12.10.2+15.04.20150205/tests/volume-control-mock.vala indicator-sound-12.10.2+15.04.20150219.1/tests/volume-control-mock.vala --- indicator-sound-12.10.2+15.04.20150205/tests/volume-control-mock.vala 1970-01-01 00:00:00.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/tests/volume-control-mock.vala 2015-02-19 16:27:42.000000000 +0000 @@ -0,0 +1,48 @@ +/* + * -*- 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 . + * + * Authors: + * Ted Gould + */ + +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; } } + private VolumeControl.Volume _vol = new VolumeControl.Volume(); + public override VolumeControl.Volume volume { get { return _vol; } set { _vol = value; }} + public override double mic_volume { get; set; } + + public override void set_mute (bool mute) { + + } + + public VolumeControlMock() { + 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 -Nru indicator-sound-12.10.2+15.04.20150205/tests/volume-control-test.cc indicator-sound-12.10.2+15.04.20150219.1/tests/volume-control-test.cc --- indicator-sound-12.10.2+15.04.20150205/tests/volume-control-test.cc 2015-02-05 17:18:57.000000000 +0000 +++ indicator-sound-12.10.2+15.04.20150219.1/tests/volume-control-test.cc 2015-02-19 16:27:24.000000000 +0000 @@ -71,13 +71,13 @@ }; 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); }