Merge lp:~ted/indicator-sound/sound-stream-cleanup into lp:indicator-sound/15.04

Proposed by Ted Gould
Status: Work in progress
Proposed branch: lp:~ted/indicator-sound/sound-stream-cleanup
Merge into: lp:indicator-sound/15.04
Prerequisite: lp:~ted/indicator-sound/indicator-test
Diff against target: 2656 lines (+1450/-591)
20 files modified
src/CMakeLists.txt (+18/-0)
src/focus-tracker-stack.vala (+88/-0)
src/focus-tracker.vala (+22/-0)
src/main.c (+73/-12)
src/media-player-list.vala (+1/-1)
src/service.vala (+95/-77)
src/sound-menu.vala (+12/-1)
src/volume-control-pulse.vala (+318/-498)
src/volume-control.vala (+31/-0)
tests/CMakeLists.txt (+30/-0)
tests/focus-tracker-mock.vala (+27/-0)
tests/gtest-gvariant.h (+110/-0)
tests/indicator-test.cc (+6/-0)
tests/media-player-list-mock.vala (+25/-0)
tests/notifications-mock.h (+155/-0)
tests/notifications-test.cc (+348/-0)
tests/pa-mock.cpp (+22/-0)
tests/volume-control-mock.vala (+43/-0)
tests/volume-control-test.cc (+4/-2)
vapi/libpulse-ext-stream-restore.vapi (+22/-0)
To merge this branch: bzr merge lp:~ted/indicator-sound/sound-stream-cleanup
Reviewer Review Type Date Requested Status
Indicator Applet Developers Pending
Review via email: mp+248180@code.launchpad.net

This proposal supersedes a proposal from 2014-12-17.

To post a comment you must log in.
503. By Ted Gould

Merging in the notifications mock branch

Unmerged revisions

503. By Ted Gould

Merging in the notifications mock branch

502. By Ted Gould

Adding another missing symbol

501. By Ted Gould

Add a mock symbol for stream restore checking

500. By Ted Gould

Add a focus tracker mock

499. By Ted Gould

Put an abstraction in for the focus tracker so we can mock it

498. By Ted Gould

Merging the indicator-test branch

497. By Ted Gould

Set it up so the end we're detecting that the stream changes, and then looking up values based on that.

496. By Ted Gould

Warn when we stop because of not being able to get a name

495. By Ted Gould

Making sure all the media player lists can use standard object primitivs

494. By Ted Gould

Bind the property into a value in volume control

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/CMakeLists.txt'
--- src/CMakeLists.txt 2015-01-27 17:42:26 +0000
+++ src/CMakeLists.txt 2015-02-14 02:38:32 +0000
@@ -36,6 +36,7 @@
36 --vapidir=.36 --vapidir=.
37 --pkg=url-dispatcher37 --pkg=url-dispatcher
38 --pkg=bus-watcher38 --pkg=bus-watcher
39 --pkg=libpulse-ext-stream-restore
39)40)
4041
41vala_add(indicator-sound-service42vala_add(indicator-sound-service
@@ -43,15 +44,23 @@
43 DEPENDS44 DEPENDS
44 sound-menu45 sound-menu
45 volume-control46 volume-control
47 volume-control-pulse
46 media-player48 media-player
47 media-player-list49 media-player-list
48 mpris2-interfaces50 mpris2-interfaces
49 accounts-service-user51 accounts-service-user
52 focus-tracker
50)53)
51vala_add(indicator-sound-service54vala_add(indicator-sound-service
52 volume-control.vala55 volume-control.vala
53)56)
54vala_add(indicator-sound-service57vala_add(indicator-sound-service
58 volume-control-pulse.vala
59 DEPENDS
60 volume-control
61 focus-tracker
62)
63vala_add(indicator-sound-service
55 media-player.vala64 media-player.vala
56)65)
57vala_add(indicator-sound-service66vala_add(indicator-sound-service
@@ -120,6 +129,14 @@
120vala_add(indicator-sound-service129vala_add(indicator-sound-service
121 greeter-broadcast.vala130 greeter-broadcast.vala
122)131)
132vala_add(indicator-sound-service
133 focus-tracker.vala
134)
135vala_add(indicator-sound-service
136 focus-tracker-stack.vala
137 DEPENDS
138 focus-tracker
139)
123140
124vala_finish(indicator-sound-service141vala_finish(indicator-sound-service
125 SOURCES142 SOURCES
@@ -154,6 +171,7 @@
154171
155add_definitions(172add_definitions(
156 -w173 -w
174 -DG_LOG_DOMAIN="indicator-sound"
157)175)
158176
159add_library(177add_library(
160178
=== added file 'src/focus-tracker-stack.vala'
--- src/focus-tracker-stack.vala 1970-01-01 00:00:00 +0000
+++ src/focus-tracker-stack.vala 2015-02-14 02:38:32 +0000
@@ -0,0 +1,88 @@
1/*
2 * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
3 * Copyright © 2014 Canonical Ltd.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors:
18 * Ted Gould <ted@canonical.com>
19 */
20
21private struct WindowInfo {
22 public uint window_id;
23 public string app_id;
24 public bool focused;
25 public uint stage;
26}
27
28/*
29private struct DesktopInfo {
30 public string app_id;
31 public string desktop_file;
32}
33*/
34
35[DBus (name = "com.canonical.Unity.WindowStack")]
36private interface WindowStack : Object {
37 /* public abstract async DesktopInfo GetAppIdFromPid (uint pid) throws IOError; */
38 public abstract async WindowInfo[] GetWindowStack () throws IOError;
39 /* public abstract async string[] GetWindowProperties (uint window_id, string app_id, string[] names) throws IOError; */
40
41 public signal void FocusedWindowChanged (uint window_id, string app_id, uint stage);
42 /* public signal void WindowCreated (uint window_id, string app_id); */
43 /* public signal void WindowDestroyed (uint window_id, string app_id); */
44}
45
46public class FocusTrackerStack : FocusTracker {
47 public override string focused_appid { get; private set; default = "unknown"; }
48 private WindowStack proxy;
49
50 public FocusTrackerStack ( ) {
51 build_proxies.begin();
52 }
53
54 private async void build_proxies() {
55 try {
56 proxy = yield Bus.get_proxy<WindowStack>(BusType.SESSION,
57 "com.canonical.Unity.WindowStack",
58 "/com/canonical/Unity/WindowStack",
59 DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
60 null);
61
62 proxy.FocusedWindowChanged.connect((window, appid, stage) => {
63 if (stage == 0) {
64 debug("Focus changed to: %s", appid);
65 this.focused_appid = appid;
66 }
67 });
68
69 update_focused_appid.begin();
70 } catch (Error e) {
71 warning("Unable to create window stack proxy: %s", e.message);
72 }
73 }
74
75 private async void update_focused_appid () {
76 try {
77 var windows = yield proxy.GetWindowStack();
78 foreach (var window in windows) {
79 if (window.focused && window.stage == 0) {
80 focused_appid = window.app_id;
81 break;
82 }
83 }
84 } catch (Error e) {
85 warning("Unable to get window stack list: %s", e.message);
86 }
87 }
88}
089
=== added file 'src/focus-tracker.vala'
--- src/focus-tracker.vala 1970-01-01 00:00:00 +0000
+++ src/focus-tracker.vala 2015-02-14 02:38:32 +0000
@@ -0,0 +1,22 @@
1/*
2 * Copyright © 2015 Canonical Ltd.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors:
17 * Ted Gould <ted@canonical.com>
18 */
19
20public abstract class FocusTracker : Object {
21 public virtual string focused_appid { get; protected set; }
22}
023
=== modified file 'src/main.c'
--- src/main.c 2014-02-25 22:47:45 +0000
+++ src/main.c 2015-02-14 02:38:32 +0000
@@ -1,6 +1,21 @@
1/* main.c generated by valac 0.22.1, the Vala compiler1/*
2 * generated from main.vala, do not modify */2 * Copyright © 2015 Canonical Ltd.
33 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors:
17 * Ted Gould <ted@canonical.com>
18 */
419
5#include <glib.h>20#include <glib.h>
6#include <locale.h>21#include <locale.h>
@@ -9,33 +24,79 @@
9#include "indicator-sound-service.h"24#include "indicator-sound-service.h"
10#include "config.h"25#include "config.h"
1126
27static gboolean
28sigterm_handler (gpointer data)
29{
30 g_debug("Got SIGTERM");
31 g_main_loop_quit((GMainLoop *)data);
32 return G_SOURCE_REMOVE;
33}
34
35static void
36name_lost (GDBusConnection * connection, const gchar * name, gpointer user_data)
37{
38 g_debug("Name lost");
39 g_main_loop_quit((GMainLoop *)user_data);
40}
41
12int42int
13main (int argc, char ** argv) {43main (int argc, char ** argv) {
14 gint result = 0;44 GMainLoop * loop = NULL;
15 IndicatorSoundService* service = NULL;45 IndicatorSoundService* service = NULL;
46 GDBusConnection * bus = NULL;
1647
17 bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");48 bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
18 setlocale (LC_ALL, "");49 setlocale (LC_ALL, "");
19 bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);50 bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
2051
52 /* Grab DBus */
53 GError * error = NULL;
54 bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
55 if (error != NULL) {
56 g_error("Unable to get session bus: %s", error->message);
57 g_error_free(error);
58 return -1;
59 }
60
61 /* Build Mainloop */
62 loop = g_main_loop_new(NULL, FALSE);
63
64 g_unix_signal_add(SIGTERM, sigterm_handler, loop);
65
21 /* Initialize libnotify */66 /* Initialize libnotify */
22 notify_init ("indicator-sound");67 notify_init ("indicator-sound");
2368
24 MediaPlayerList * playerlist = NULL;69 MediaPlayerList * playerlist = NULL;
70 AccountsServiceUser * accounts = NULL;
2571
26 if (g_strcmp0("lightdm", g_get_user_name()) == 0) {72 if (g_strcmp0("lightdm", g_get_user_name()) == 0) {
27 playerlist = MEDIA_PLAYER_LIST(media_player_list_greeter_new());73 playerlist = MEDIA_PLAYER_LIST(media_player_list_greeter_new());
28 } else {74 } else {
29 playerlist = MEDIA_PLAYER_LIST(media_player_list_mpris_new());75 playerlist = MEDIA_PLAYER_LIST(media_player_list_mpris_new());
76 accounts = accounts_service_user_new();
30 }77 }
3178
32 service = indicator_sound_service_new (playerlist);79 FocusTracker * tracker = FOCUS_TRACKER(focus_tracker_stack_new());
33 result = indicator_sound_service_run (service);80 VolumeControlPulse * volume = volume_control_pulse_new(tracker);
3481
35 g_object_unref(playerlist);82 service = indicator_sound_service_new (playerlist, volume, accounts);
36 g_object_unref(service);83
3784 g_bus_own_name_on_connection(bus,
38 return result;85 "com.canonical.indicator.sound",
86 G_BUS_NAME_OWNER_FLAGS_NONE,
87 NULL, /* acquired */
88 name_lost,
89 loop,
90 NULL);
91
92 g_main_loop_run(loop);
93
94 g_clear_object(&playerlist);
95 g_clear_object(&tracker);
96 g_clear_object(&accounts);
97 g_clear_object(&service);
98 g_clear_object(&bus);
99
100 return 0;
39}101}
40102
41
42103
=== modified file 'src/media-player-list.vala'
--- src/media-player-list.vala 2014-02-24 22:47:50 +0000
+++ src/media-player-list.vala 2015-02-14 02:38:32 +0000
@@ -17,7 +17,7 @@
17 * Ted Gould <ted@canonical.com>17 * Ted Gould <ted@canonical.com>
18 */18 */
1919
20public class MediaPlayerList {20public abstract class MediaPlayerList : Object {
21 public class Iterator {21 public class Iterator {
22 public virtual MediaPlayer? next_value() {22 public virtual MediaPlayer? next_value() {
23 return null;23 return null;
2424
=== modified file 'src/service.vala'
--- src/service.vala 2015-02-05 14:52:58 +0000
+++ src/service.vala 2015-02-14 02:38:32 +0000
@@ -18,13 +18,29 @@
18 */18 */
1919
20public class IndicatorSound.Service: Object {20public class IndicatorSound.Service: Object {
21 public Service (MediaPlayerList playerlist) {21 DBusConnection bus;
22 DBusProxy notification_proxy;
23
24 public Service (MediaPlayerList playerlist, VolumeControl volume, AccountsServiceUser? accounts) {
25 try {
26 bus = Bus.get_sync(GLib.BusType.SESSION);
27 } catch (GLib.Error e) {
28 error("Unable to get DBus session bus: %s", e.message);
29 }
30
22 sync_notification = new Notify.Notification(_("Volume"), "", "audio-volume-muted");31 sync_notification = new Notify.Notification(_("Volume"), "", "audio-volume-muted");
23 this.notification_server_watch = GLib.Bus.watch_name(GLib.BusType.SESSION,32 try {
24 "org.freedesktop.Notifications",33 this.notification_proxy = new DBusProxy.for_bus_sync(GLib.BusType.SESSION,
25 GLib.BusNameWatcherFlags.NONE,34 DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | DBusProxyFlags.DO_NOT_CONNECT_SIGNALS | DBusProxyFlags.DO_NOT_AUTO_START,
26 () => { check_sync_notification = false; },35 null, /* interface info */
27 () => { check_sync_notification = false; });36 "org.freedesktop.Notifications",
37 "/org/freedesktop/Notifications",
38 "org.freedesktop.Notifications",
39 null);
40 this.notification_proxy.notify["g-name-owner"].connect ( () => { debug("Notifications name owner changed"); check_sync_notification = false; } );
41 } catch (GLib.Error e) {
42 error("Unable to build notification proxy: %s", e.message);
43 }
2844
29 this.settings = new Settings ("com.canonical.indicator.sound");45 this.settings = new Settings ("com.canonical.indicator.sound");
30 this.sharedsettings = new Settings ("com.ubuntu.sound");46 this.sharedsettings = new Settings ("com.ubuntu.sound");
@@ -32,12 +48,12 @@
32 this.settings.bind ("visible", this, "visible", SettingsBindFlags.GET);48 this.settings.bind ("visible", this, "visible", SettingsBindFlags.GET);
33 this.notify["visible"].connect ( () => this.update_root_icon () );49 this.notify["visible"].connect ( () => this.update_root_icon () );
3450
35 this.volume_control = new VolumeControl ();51 this.volume_control = volume;
52 this.volume_control.bind_property ("media-playing", this, "player-playing", BindingFlags.SYNC_CREATE);
3653
54 this.accounts_service = accounts;
37 /* If we're on the greeter, don't export */55 /* If we're on the greeter, don't export */
38 if (GLib.Environment.get_user_name() != "lightdm") {56 if (this.accounts_service != null) {
39 this.accounts_service = new AccountsServiceUser();
40
41 this.accounts_service.notify["showDataOnGreeter"].connect(() => {57 this.accounts_service.notify["showDataOnGreeter"].connect(() => {
42 this.export_to_accounts_service = this.accounts_service.showDataOnGreeter;58 this.export_to_accounts_service = this.accounts_service.showDataOnGreeter;
43 eventually_update_player_actions();59 eventually_update_player_actions();
@@ -90,9 +106,27 @@
90 }106 }
91 }107 }
92 });108 });
109
110 /* Everything is built, let's put it on the bus */
111 try {
112 export_actions = bus.export_action_group ("/com/canonical/indicator/sound", this.actions);
113 } catch (Error e) {
114 critical ("%s", e.message);
115 }
116
117 this.menus.@foreach ( (profile, menu) => menu.export (bus, @"/com/canonical/indicator/sound/$profile"));
93 }118 }
94119
95 ~Service() {120 ~Service() {
121 debug("Destroying Service Object");
122
123 clear_acts_player();
124
125 if (this.player_action_update_id > 0) {
126 Source.remove (this.player_action_update_id);
127 this.player_action_update_id = 0;
128 }
129
96 if (this.sound_was_blocked_timeout_id > 0) {130 if (this.sound_was_blocked_timeout_id > 0) {
97 Source.remove (this.sound_was_blocked_timeout_id);131 Source.remove (this.sound_was_blocked_timeout_id);
98 this.sound_was_blocked_timeout_id = 0;132 this.sound_was_blocked_timeout_id = 0;
@@ -102,6 +136,11 @@
102 GLib.Bus.unwatch_name(this.notification_server_watch);136 GLib.Bus.unwatch_name(this.notification_server_watch);
103 this.notification_server_watch = 0;137 this.notification_server_watch = 0;
104 }138 }
139
140 if (this.export_actions != 0) {
141 bus.unexport_action_group(this.export_actions);
142 this.export_actions = 0;
143 }
105 }144 }
106145
107 bool greeter_show_track () {146 bool greeter_show_track () {
@@ -115,30 +154,6 @@
115 this.accounts_service.player = null;154 this.accounts_service.player = null;
116 }155 }
117156
118 public int run () {
119 if (this.loop != null) {
120 warning ("service is already running");
121 return 1;
122 }
123
124 Bus.own_name (BusType.SESSION, "com.canonical.indicator.sound", BusNameOwnerFlags.NONE,
125 this.bus_acquired, null, this.name_lost);
126
127 this.loop = new MainLoop (null, false);
128
129 GLib.Unix.signal_add(GLib.ProcessSignal.TERM, () => {
130 debug("SIGTERM recieved, stopping our mainloop");
131 this.loop.quit();
132 return false;
133 });
134
135 this.loop.run ();
136
137 clear_acts_player();
138
139 return 0;
140 }
141
142 public bool visible { get; set; }157 public bool visible { get; set; }
143158
144 public bool allow_amplified_volume {159 public bool allow_amplified_volume {
@@ -171,13 +186,13 @@
171 { "indicator-shown", null, null, "@b false", null },186 { "indicator-shown", null, null, "@b false", null },
172 };187 };
173188
174 MainLoop loop;
175 SimpleActionGroup actions;189 SimpleActionGroup actions;
176 HashTable<string, SoundMenu> menus;190 HashTable<string, SoundMenu> menus;
177 Settings settings;191 Settings settings;
178 Settings sharedsettings;192 Settings sharedsettings;
179 VolumeControl volume_control;193 VolumeControl volume_control;
180 MediaPlayerList players;194 MediaPlayerList players;
195 public bool player_playing { get; set; default = false; }
181 uint player_action_update_id;196 uint player_action_update_id;
182 bool mute_blocks_sound;197 bool mute_blocks_sound;
183 uint sound_was_blocked_timeout_id;198 uint sound_was_blocked_timeout_id;
@@ -275,6 +290,7 @@
275290
276 void update_sync_notification () {291 void update_sync_notification () {
277 if (!check_sync_notification) {292 if (!check_sync_notification) {
293 support_sync_notification = false;
278 List<string> caps = Notify.get_server_caps ();294 List<string> caps = Notify.get_server_caps ();
279 if (caps.find_custom ("x-canonical-private-synchronous", strcmp) != null) {295 if (caps.find_custom ("x-canonical-private-synchronous", strcmp) != null) {
280 support_sync_notification = true;296 support_sync_notification = true;
@@ -285,27 +301,12 @@
285 if (!support_sync_notification)301 if (!support_sync_notification)
286 return;302 return;
287303
288 /* Update our volume and output */
289 var oldoutput = this.last_output_notification;
290 this.last_output_notification = this.volume_control.stream;
291
292 var oldvolume = this.last_volume_notification;
293 this.last_volume_notification = volume_control.volume;
294
295 /* Suppress notifications of volume changes if it is because the
296 output stream changed. */
297 if (oldoutput != this.last_output_notification)
298 return;
299 /* Supress updates that don't change the value */
300 if (GLib.Math.fabs(oldvolume - this.last_volume_notification) < 0.01)
301 return;
302
303 var shown_action = actions.lookup_action ("indicator-shown") as SimpleAction;304 var shown_action = actions.lookup_action ("indicator-shown") as SimpleAction;
304 if (shown_action != null && shown_action.get_state().get_boolean())305 if (shown_action != null && shown_action.get_state().get_boolean())
305 return;306 return;
306307
307 /* Determine Label */308 /* Determine Label */
308 string volume_label = "";309 string volume_label = volume_control.stream; /* TODO: Undo this */
309 if (volume_control.high_volume)310 if (volume_control.high_volume)
310 volume_label = _("High volume");311 volume_label = _("High volume");
311312
@@ -341,13 +342,14 @@
341 }342 }
342 }343 }
343344
345 SimpleAction silent_action;
344 Action create_silent_mode_action () {346 Action create_silent_mode_action () {
345 bool silentNow = false;347 bool silentNow = false;
346 if (this.accounts_service != null) {348 if (this.accounts_service != null) {
347 silentNow = this.accounts_service.silentMode;349 silentNow = this.accounts_service.silentMode;
348 }350 }
349351
350 var silent_action = new SimpleAction.stateful ("silent-mode", null, new Variant.boolean (silentNow));352 silent_action = new SimpleAction.stateful ("silent-mode", null, new Variant.boolean (silentNow));
351353
352 /* If we're not dealing with accounts service, we'll just always be out354 /* If we're not dealing with accounts service, we'll just always be out
353 of silent mode and that's cool. */355 of silent mode and that's cool. */
@@ -371,15 +373,16 @@
371 return silent_action;373 return silent_action;
372 }374 }
373375
376 SimpleAction mute_action;
374 Action create_mute_action () {377 Action create_mute_action () {
375 var mute_action = new SimpleAction.stateful ("mute", null, new Variant.boolean (this.volume_control.mute));378 mute_action = new SimpleAction.stateful ("mute", null, new Variant.boolean (this.volume_control.mute));
376379
377 mute_action.activate.connect ( (action, param) => {380 mute_action.activate.connect ( (action, param) => {
378 action.change_state (new Variant.boolean (!action.get_state ().get_boolean ()));381 action.change_state (new Variant.boolean (!action.get_state ().get_boolean ()));
379 });382 });
380383
381 mute_action.change_state.connect ( (action, val) => {384 mute_action.change_state.connect ( (action, val) => {
382 volume_control.set_mute (val.get_boolean ());385 volume_control.mute = val.get_boolean ();
383 });386 });
384387
385 this.volume_control.notify["mute"].connect ( () => {388 this.volume_control.notify["mute"].connect ( () => {
@@ -415,6 +418,7 @@
415 return mute_action;418 return mute_action;
416 }419 }
417420
421 SimpleAction volume_action;
418 Action create_volume_action () {422 Action create_volume_action () {
419 /* The action's state is between be in [0.0, 1.0] instead of [0.0,423 /* The action's state is between be in [0.0, 1.0] instead of [0.0,
420 * max_volume], so that we don't need to update the slider menu item424 * max_volume], so that we don't need to update the slider menu item
@@ -425,7 +429,7 @@
425429
426 double volume = this.volume_control.volume / this.max_volume;430 double volume = this.volume_control.volume / this.max_volume;
427431
428 var volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, new Variant.double (volume));432 volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, new Variant.double (volume));
429433
430 volume_action.change_state.connect ( (action, val) => {434 volume_action.change_state.connect ( (action, val) => {
431 double v = val.get_double () * this.max_volume;435 double v = val.get_double () * this.max_volume;
@@ -440,12 +444,26 @@
440 });444 });
441445
442 this.volume_control.notify["volume"].connect (() => {446 this.volume_control.notify["volume"].connect (() => {
443 var vol_action = this.actions.lookup_action ("volume") as SimpleAction;
444
445 /* Normalize volume, because the volume action's state is [0.0, 1.0], see create_volume_action() */447 /* Normalize volume, because the volume action's state is [0.0, 1.0], see create_volume_action() */
446 vol_action.set_state (new Variant.double (this.volume_control.volume / this.max_volume));448 volume_action.set_state (new Variant.double (this.volume_control.volume / this.max_volume));
447449
448 this.update_root_icon ();450 this.update_root_icon ();
451
452 /* Update our volume and output */
453 var oldoutput = this.last_output_notification;
454 this.last_output_notification = this.volume_control.stream;
455
456 var oldvolume = this.last_volume_notification;
457 this.last_volume_notification = volume_control.volume;
458
459 /* Suppress notifications of volume changes if it is because the
460 output stream changed. */
461 if (oldoutput != this.last_output_notification)
462 return;
463 /* Supress updates that don't change the value */
464 if (GLib.Math.fabs(oldvolume - this.last_volume_notification) < 0.01)
465 return;
466
449 this.update_sync_notification ();467 this.update_sync_notification ();
450 });468 });
451469
@@ -454,24 +472,26 @@
454 return volume_action;472 return volume_action;
455 }473 }
456474
475 SimpleAction mic_volume_action;
457 Action create_mic_volume_action () {476 Action create_mic_volume_action () {
458 var volume_action = new SimpleAction.stateful ("mic-volume", null, new Variant.double (this.volume_control.mic_volume));477 mic_volume_action = new SimpleAction.stateful ("mic-volume", null, new Variant.double (this.volume_control.mic_volume));
459478
460 volume_action.change_state.connect ( (action, val) => {479 mic_volume_action.change_state.connect ( (action, val) => {
461 volume_control.mic_volume = val.get_double ();480 volume_control.mic_volume = val.get_double ();
462 });481 });
463482
464 this.volume_control.notify["mic-volume"].connect ( () => {483 this.volume_control.notify["mic-volume"].connect ( () => {
465 volume_action.set_state (new Variant.double (this.volume_control.mic_volume));484 mic_volume_action.set_state (new Variant.double (this.volume_control.mic_volume));
466 });485 });
467486
468 this.volume_control.bind_property ("ready", volume_action, "enabled", BindingFlags.SYNC_CREATE);487 this.volume_control.bind_property ("ready", mic_volume_action, "enabled", BindingFlags.SYNC_CREATE);
469488
470 return volume_action;489 return mic_volume_action;
471 }490 }
472491
492 SimpleAction high_volume_action;
473 Action create_high_volume_action () {493 Action create_high_volume_action () {
474 var high_volume_action = new SimpleAction.stateful("high-volume", null, new Variant.boolean (this.volume_control.high_volume));494 high_volume_action = new SimpleAction.stateful("high-volume", null, new Variant.boolean (this.volume_control.high_volume));
475495
476 this.volume_control.notify["high-volume"].connect( () => {496 this.volume_control.notify["high-volume"].connect( () => {
477 high_volume_action.set_state(new Variant.boolean (this.volume_control.high_volume));497 high_volume_action.set_state(new Variant.boolean (this.volume_control.high_volume));
@@ -481,19 +501,7 @@
481 return high_volume_action;501 return high_volume_action;
482 }502 }
483503
484 void bus_acquired (DBusConnection connection, string name) {504 uint export_actions = 0;
485 try {
486 connection.export_action_group ("/com/canonical/indicator/sound", this.actions);
487 } catch (Error e) {
488 critical ("%s", e.message);
489 }
490
491 this.menus.@foreach ( (profile, menu) => menu.export (connection, @"/com/canonical/indicator/sound/$profile"));
492 }
493
494 void name_lost (DBusConnection connection, string name) {
495 this.loop.quit ();
496 }
497505
498 Variant action_state_for_player (MediaPlayer player, bool show_track = true) {506 Variant action_state_for_player (MediaPlayer player, bool show_track = true) {
499 var builder = new VariantBuilder (new VariantType ("a{sv}"));507 var builder = new VariantBuilder (new VariantType ("a{sv}"));
@@ -510,6 +518,7 @@
510518
511 bool update_player_actions () {519 bool update_player_actions () {
512 bool clear_accounts_player = true;520 bool clear_accounts_player = true;
521 bool player_playing = false;
513522
514 foreach (var player in this.players) {523 foreach (var player in this.players) {
515 SimpleAction? action = this.actions.lookup_action (player.id) as SimpleAction;524 SimpleAction? action = this.actions.lookup_action (player.id) as SimpleAction;
@@ -529,11 +538,20 @@
529 accounts_service.player = player;538 accounts_service.player = player;
530 clear_accounts_player = false;539 clear_accounts_player = false;
531 }540 }
541
542 if (player.state == "Playing") {
543 player_playing = true;
544 }
532 }545 }
533546
534 if (clear_accounts_player)547 if (clear_accounts_player)
535 clear_acts_player();548 clear_acts_player();
536549
550 if (this.player_playing != player_playing) {
551 this.player_playing = player_playing;
552 update_root_icon();
553 }
554
537 this.player_action_update_id = 0;555 this.player_action_update_id = 0;
538 return false;556 return false;
539 }557 }
540558
=== modified file 'src/sound-menu.vala'
--- src/sound-menu.vala 2015-01-29 17:31:03 +0000
+++ src/sound-menu.vala 2015-02-14 02:38:32 +0000
@@ -73,9 +73,20 @@
73 this.greeter_players = (flags & DisplayFlags.GREETER_PLAYERS) != 0;73 this.greeter_players = (flags & DisplayFlags.GREETER_PLAYERS) != 0;
74 }74 }
7575
76 ~SoundMenu () {
77 if (export_id != 0) {
78 bus.unexport_menu_model(export_id);
79 export_id = 0;
80 }
81 }
82
83 DBusConnection? bus = null;
84 uint export_id = 0;
85
76 public void export (DBusConnection connection, string object_path) {86 public void export (DBusConnection connection, string object_path) {
87 bus = connection;
77 try {88 try {
78 connection.export_menu_model (object_path, this.root);89 export_id = bus.export_menu_model (object_path, this.root);
79 } catch (Error e) {90 } catch (Error e) {
80 critical ("%s", e.message);91 critical ("%s", e.message);
81 }92 }
8293
=== renamed file 'src/volume-control.vala' => 'src/volume-control-pulse.vala'
--- src/volume-control.vala 2015-01-30 14:57:03 +0000
+++ src/volume-control-pulse.vala 2015-02-14 02:38:32 +0000
@@ -19,6 +19,7 @@
19 */19 */
2020
21using PulseAudio;21using PulseAudio;
22using PulseAudio.Extension.StreamRestore;
22using Notify;23using Notify;
23using Gee;24using Gee;
2425
@@ -32,42 +33,51 @@
32 public signal void entry_selected (string entry_name);33 public signal void entry_selected (string entry_name);
33}34}
3435
35public class VolumeControl : Object36public class VolumeControlPulse : VolumeControl
36{37{
37 /* this is static to ensure it being freed after @context (loop does not have ref counting) */38 /* this is static to ensure it being freed after @context (loop does not have ref counting) */
38 private static PulseAudio.GLibMainLoop loop;39 private static PulseAudio.GLibMainLoop loop;
3940
41 private FocusTracker focus_tracker;
42 public bool media_playing { get; set; default = false; }
43
40 private uint _reconnect_timer = 0;44 private uint _reconnect_timer = 0;
4145
42 private PulseAudio.Context context;46 private PulseAudio.Context context;
43 private bool _mute = true;47 private bool _mute = true;
44 private bool _is_playing = false;48 public override bool is_playing { get; private set; default = false; }
45 private double _volume = 0.0;49 private double _volume = 0.0;
46 private double _mic_volume = 0.0;50 private double _mic_volume = 0.0;
4751
48 /* Used by the pulseaudio stream restore extension */52 /* Used by the pulseaudio stream restore extension */
49 private DBusConnection _pconn;
50 /* Need both the list and hash so we can retrieve the last known sink-input after
51 * releasing the current active one (restoring back to the previous known role) */
52 private Gee.ArrayList<uint32> _sink_input_list = new Gee.ArrayList<uint32> ();
53 private HashMap<uint32, string> _sink_input_hash = new HashMap<uint32, string> ();
54 private bool _pulse_use_stream_restore = false;53 private bool _pulse_use_stream_restore = false;
55 private uint32 _active_sink_input = -1;54 private enum InputStreamType {
56 private string[] _valid_roles = {"multimedia", "alert", "alarm", "phone"};55 ALERT = 0,
57 public string stream {56 ALARM = 1,
57 MULTIMEDIA = 2,
58 PHONE = 3
59 }
60 private InputStreamType currentstream = InputStreamType.ALERT;
61 private InputStreamType tempstream = InputStreamType.ALERT;
62 /** The name of the stream that we're currently playing */
63 public override string stream {
58 get {64 get {
59 if (_active_sink_input < 0 || _active_sink_input >= _valid_roles.length)65 switch (this.currentstream) {
60 return "multimedia";66 case InputStreamType.ALERT:
61 else67 return "alert";
62 return _valid_roles[_active_sink_input];68 case InputStreamType.ALARM:
69 return "alarm";
70 case InputStreamType.MULTIMEDIA:
71 return "multimedia";
72 case InputStreamType.PHONE:
73 return "phone";
74 }
75
76 return "alert";
63 }77 }
64 }78 }
65 private string? _objp_role_multimedia = null;
66 private string? _objp_role_alert = null;
67 private string? _objp_role_alarm = null;
68 private string? _objp_role_phone = null;
69 private uint _pa_volume_sig_count = 0;
7079
80 /* Accounts Service Variables */
71 private DBusProxy _user_proxy;81 private DBusProxy _user_proxy;
72 private GreeterListInterface _greeter_proxy;82 private GreeterListInterface _greeter_proxy;
73 private Cancellable _mute_cancellable;83 private Cancellable _mute_cancellable;
@@ -76,23 +86,38 @@
76 private uint _accountservice_volume_timer = 0;86 private uint _accountservice_volume_timer = 0;
77 private bool _send_next_local_volume = false;87 private bool _send_next_local_volume = false;
78 private double _account_service_volume = 0.0;88 private double _account_service_volume = 0.0;
89
90
79 private bool _active_port_headphone = false;91 private bool _active_port_headphone = false;
92 /* There is not easy way to check if the port is a headset/headphone besides
93 * checking for the port name. On touch (with the pulseaudio droid element)
94 * the headset/headphone port is called 'output-headset' and 'output-headphone'.
95 * On the desktop this is usually called 'analog-output-headphones' */
96 private string[] headphone_outputs = {"output-wired_headset", "output-wired_headphone", "analog-output-headphones"};
8097
81 /** true when connected to the pulse server */98 /** true when connected to the pulse server */
82 public bool ready { get; set; }99 public override bool ready { get; private set; }
83100
84 /** true when a microphone is active **/101 /** true when a microphone is active **/
85 public bool active_mic { get; private set; default = false; }102 public override bool active_mic { get; private set; default = false; }
86103
87 /** true when high volume warnings should be shown */104 /** true when high volume warnings should be shown */
88 public bool high_volume {105 public override bool high_volume {
89 get {106 get {
90 return this._volume > 0.75 && _active_port_headphone; 107 return this._volume > 0.75 && _active_port_headphone && stream == "multimedia";
91 }108 }
92 }109 }
93110
94 public VolumeControl ()111 public VolumeControlPulse (FocusTracker tracker)
95 {112 {
113 this.focus_tracker = tracker;
114 this.focus_tracker.notify["focused-appid"].connect(() => {
115 if (!this.ready)
116 return;
117
118 this.update_sink_input();
119 });
120
96 if (loop == null)121 if (loop == null)
97 loop = new PulseAudio.GLibMainLoop ();122 loop = new PulseAudio.GLibMainLoop ();
98123
@@ -102,9 +127,16 @@
102 setup_accountsservice.begin ();127 setup_accountsservice.begin ();
103128
104 this.reconnect_to_pulse ();129 this.reconnect_to_pulse ();
130
131 this.notify["media-playing"].connect(() => {
132 update_sink_input();
133 });
134 this.notify["stream"].connect(() => {
135 update_stream();
136 });
105 }137 }
106138
107 ~VolumeControl ()139 ~VolumeControlPulse ()
108 {140 {
109 if (_reconnect_timer != 0) {141 if (_reconnect_timer != 0) {
110 Source.remove (_reconnect_timer);142 Source.remove (_reconnect_timer);
@@ -114,7 +146,10 @@
114 stop_account_service_volume_timer();146 stop_account_service_volume_timer();
115 }147 }
116148
117 /* PulseAudio logic*/149 /************************/
150 /* PulseAudio logic */
151 /************************/
152
118 private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index)153 private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index)
119 {154 {
120 switch (t & Context.SubscriptionEventType.FACILITY_MASK)155 switch (t & Context.SubscriptionEventType.FACILITY_MASK)
@@ -124,23 +159,7 @@
124 break;159 break;
125160
126 case Context.SubscriptionEventType.SINK_INPUT:161 case Context.SubscriptionEventType.SINK_INPUT:
127 switch (t & Context.SubscriptionEventType.TYPE_MASK)162 update_sink_input ();
128 {
129 case Context.SubscriptionEventType.NEW:
130 c.get_sink_input_info (index, handle_new_sink_input_cb);
131 break;
132
133 case Context.SubscriptionEventType.CHANGE:
134 c.get_sink_input_info (index, handle_changed_sink_input_cb);
135 break;
136
137 case Context.SubscriptionEventType.REMOVE:
138 remove_sink_input_from_list (index);
139 break;
140 default:
141 debug ("Sink input event not known.");
142 break;
143 }
144 break;163 break;
145164
146 case Context.SubscriptionEventType.SOURCE:165 case Context.SubscriptionEventType.SOURCE:
@@ -151,7 +170,14 @@
151 switch (t & Context.SubscriptionEventType.TYPE_MASK)170 switch (t & Context.SubscriptionEventType.TYPE_MASK)
152 {171 {
153 case Context.SubscriptionEventType.NEW:172 case Context.SubscriptionEventType.NEW:
154 c.get_source_output_info (index, source_output_info_cb);173 c.get_source_output_info (index, (context, info, eol) => {
174 if (info == null)
175 return;
176
177 var role = info.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
178 if (role == "phone" || role == "production")
179 this.active_mic = true;
180 });
155 break;181 break;
156182
157 case Context.SubscriptionEventType.REMOVE:183 case Context.SubscriptionEventType.REMOVE:
@@ -162,291 +188,205 @@
162 }188 }
163 }189 }
164190
165 private void sink_info_cb_for_props (Context c, SinkInfo? i, int eol)
166 {
167 bool old_high_volume = this.high_volume;
168
169 if (i == null)
170 return;
171
172 if (_mute != (bool)i.mute)
173 {
174 _mute = (bool)i.mute;
175 this.notify_property ("mute");
176 }
177
178 var playing = (i.state == PulseAudio.SinkState.RUNNING);
179 if (_is_playing != playing)
180 {
181 _is_playing = playing;
182 this.notify_property ("is-playing");
183 }
184
185 /* Check if the current active port is headset/headphone */
186 /* There is not easy way to check if the port is a headset/headphone besides
187 * checking for the port name. On touch (with the pulseaudio droid element)
188 * the headset/headphone port is called 'output-headset' and 'output-headphone'.
189 * On the desktop this is usually called 'analog-output-headphones' */
190 if (i.active_port != null &&
191 (i.active_port.name == "output-wired_headset" ||
192 i.active_port.name == "output-wired_headphone" ||
193 i.active_port.name == "analog-output-headphones")) {
194 _active_port_headphone = true;
195 } else {
196 _active_port_headphone = false;
197 }
198
199 if (_pulse_use_stream_restore == false &&
200 _volume != volume_to_double (i.volume.max ()))
201 {
202 _volume = volume_to_double (i.volume.max ());
203 this.notify_property("volume");
204 start_local_volume_timer();
205 }
206
207 if (this.high_volume != old_high_volume) {
208 this.notify_property("high-volume");
209 }
210 }
211
212 private void source_info_cb (Context c, SourceInfo? i, int eol)
213 {
214 if (i == null)
215 return;
216
217 if (_mic_volume != volume_to_double (i.volume.values[0]))
218 {
219 _mic_volume = volume_to_double (i.volume.values[0]);
220 this.notify_property ("mic-volume");
221 }
222 }
223
224 private void server_info_cb_for_props (Context c, ServerInfo? i)
225 {
226 if (i == null)
227 return;
228 context.get_sink_info_by_name (i.default_sink_name, sink_info_cb_for_props);
229 }
230
231 private void update_sink ()191 private void update_sink ()
232 {192 {
233 context.get_server_info (server_info_cb_for_props);193 if (!this.ready)
234 }194 return;
235195
236 private void update_source_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) {196 context.get_server_info ((context, info) => {
237 if (i != null)197 if (info == null)
238 context.get_source_info_by_name (i.default_source_name, source_info_cb);198 return;
199
200 context.get_sink_info_by_name (info.default_sink_name, (context, info, eol) => {
201 bool old_high_volume = this.high_volume;
202
203 if (info == null)
204 return;
205
206 if (_mute != (bool)info.mute)
207 {
208 _mute = (bool)info.mute;
209 this.notify_property ("mute");
210 }
211
212 this.is_playing = (info.state == PulseAudio.SinkState.RUNNING);
213
214 /* Check if the current active port is headset/headphone */
215 if (info.active_port.name in headphone_outputs) {
216 _active_port_headphone = true;
217 } else {
218 _active_port_headphone = false;
219 }
220
221 if (_pulse_use_stream_restore == false &&
222 _volume != volume_to_double (info.volume.max ()))
223 {
224 _volume = volume_to_double (info.volume.max ());
225 this.notify_property("volume");
226 start_local_volume_timer();
227 }
228
229 if (this.high_volume != old_high_volume) {
230 this.notify_property("high-volume");
231 }
232 });
233 });
234 }
235
236 private void update_sink_input ()
237 {
238 if (!this.ready)
239 return;
240
241 if (!this._pulse_use_stream_restore)
242 return;
243
244 this.tempstream = InputStreamType.ALERT;
245 if (this.media_playing)
246 this.tempstream = InputStreamType.MULTIMEDIA;
247
248 context.get_sink_input_info_list((context, info, eol) => {
249 var inputstream = InputStreamType.ALARM;
250
251 if (info != null) {
252 var role = info.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
253 var corked = (info.corked != 0);
254 var appid = info.proplist.gets (PulseAudio.Proplist.PROP_APPLICATION_ID);
255
256 /* Override the corked value for the phone as it's always corked */
257 if (role != null && role == "phone")
258 corked = false;
259
260 if (!corked && role != null && appid == this.focus_tracker.focused_appid) {
261 switch (role) {
262 case "phone":
263 inputstream = InputStreamType.PHONE;
264 break;
265 case "alarm":
266 inputstream = InputStreamType.ALARM;
267 break;
268 case "alert":
269 inputstream = InputStreamType.ALERT;
270 break;
271 case "multimedia":
272 default:
273 /* We treat unknown roles (i.e. 'music') as multimedia */
274 inputstream = InputStreamType.MULTIMEDIA;
275 break;
276 }
277 }
278 }
279
280 if (inputstream > this.tempstream) {
281 this.tempstream = inputstream;
282 }
283
284 if (eol != 0) {
285 if (this.currentstream != this.tempstream) {
286 this.currentstream = this.tempstream;
287 debug("Current stream: %s", this.stream);
288 this.notify_property("stream");
289 }
290 }
291 });
292 }
293
294 /* Look at a given profile or role and figure out what our volume and
295 mute settings should be based on that. */
296 private void update_stream ()
297 {
298
299
239 }300 }
240301
241 private void update_source ()302 private void update_source ()
242 {303 {
243 context.get_server_info (update_source_get_server_info_cb);304 if (!this.ready)
244 }305 return;
245306
246 private DBusMessage pulse_dbus_filter (DBusConnection connection, owned DBusMessage message, bool incoming)307 context.get_server_info ((context, info) => {
247 {308 if (info == null)
248 if (message.get_message_type () == DBusMessageType.SIGNAL) {309 return;
249 string active_role_objp = _objp_role_alert;310
250 if (_active_sink_input != -1)311 context.get_source_info_by_name (info.default_source_name, (context, info, eol) => {
251 active_role_objp = _sink_input_hash.get (_active_sink_input);312 if (info == null)
252313 return;
253 if (message.get_path () == active_role_objp && message.get_member () == "VolumeUpdated") {314
254 uint sig_count = 0;315 if (_mic_volume != volume_to_double (info.volume.values[0]))
255 lock (_pa_volume_sig_count) {
256 sig_count = _pa_volume_sig_count;
257 if (_pa_volume_sig_count > 0)
258 _pa_volume_sig_count--;
259 }
260
261 /* We only care about signals if our internal count is zero */
262 if (sig_count == 0) {
263 /* Extract volume and make sure it's not a side effect of us setting it */
264 Variant body = message.get_body ();
265 Variant varray = body.get_child_value (0);
266
267 uint32 type = 0, volume = 0;
268 VariantIter iter = varray.iterator ();
269 iter.next ("(uu)", &type, &volume);
270 /* Here we need to compare integer values to avoid rounding issues, so just
271 * using the volume values used by pulseaudio */
272 PulseAudio.Volume cvolume = double_to_volume (_volume);
273 if (volume != cvolume) {
274 /* Someone else changed the volume for this role, reflect on the indicator */
275 _volume = volume_to_double (volume);
276 this.notify_property("volume");
277 start_local_volume_timer();
278 }
279 }
280 }
281 }
282
283 return message;
284 }
285
286 private async void update_active_sink_input (uint32 index)
287 {
288 if ((index == -1) || (index != _active_sink_input && index in _sink_input_list)) {
289 string sink_input_objp = _objp_role_alert;
290 if (index != -1)
291 sink_input_objp = _sink_input_hash.get (index);
292 _active_sink_input = index;
293
294 /* Listen for role volume changes from pulse itself (external clients) */
295 try {
296 var builder = new VariantBuilder (new VariantType ("ao"));
297 builder.add ("o", sink_input_objp);
298
299 yield _pconn.call ("org.PulseAudio.Core1", "/org/pulseaudio/core1",
300 "org.PulseAudio.Core1", "ListenForSignal",
301 new Variant ("(sao)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry.VolumeUpdated", builder),
302 null, DBusCallFlags.NONE, -1);
303 } catch (GLib.Error e) {
304 warning ("unable to listen for pulseaudio dbus signals (%s)", e.message);
305 }
306
307 try {
308 var props_variant = yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry",
309 sink_input_objp, "org.freedesktop.DBus.Properties", "Get",
310 new Variant ("(ss)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume"),
311 null, DBusCallFlags.NONE, -1);
312 Variant tmp;
313 props_variant.get ("(v)", out tmp);
314 uint32 type = 0, volume = 0;
315 VariantIter iter = tmp.iterator ();
316 iter.next ("(uu)", &type, &volume);
317
318 _volume = volume_to_double (volume);
319 this.notify_property("volume");
320 start_local_volume_timer();
321 } catch (GLib.Error e) {
322 warning ("unable to get volume for active role %s (%s)", sink_input_objp, e.message);
323 }
324 }
325 }
326
327 private void add_sink_input_into_list (SinkInputInfo sink_input)
328 {
329 /* We're only adding ones that are not corked and with a valid role */
330 var role = sink_input.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
331
332 if (role != null && role in _valid_roles) {
333 if (sink_input.corked == 0 || role == "phone") {
334 _sink_input_list.insert (0, sink_input.index);
335 switch (role)
336 {316 {
337 case "multimedia":317 _mic_volume = volume_to_double (info.volume.values[0]);
338 _sink_input_hash.set (sink_input.index, _objp_role_multimedia);318 this.notify_property ("mic-volume");
339 break;
340 case "alert":
341 _sink_input_hash.set (sink_input.index, _objp_role_alert);
342 break;
343 case "alarm":
344 _sink_input_hash.set (sink_input.index, _objp_role_alarm);
345 break;
346 case "phone":
347 _sink_input_hash.set (sink_input.index, _objp_role_phone);
348 break;
349 }319 }
350 /* Only switch the active sink input in case a phone one is not active */320 });
351 if (_active_sink_input == -1 ||321 });
352 _sink_input_hash.get (_active_sink_input) != _objp_role_phone)
353 update_active_sink_input.begin (sink_input.index);
354 }
355 }
356 }
357
358 private void remove_sink_input_from_list (uint32 index)
359 {
360 if (index in _sink_input_list) {
361 _sink_input_list.remove (index);
362 _sink_input_hash.unset (index);
363 if (index == _active_sink_input) {
364 if (_sink_input_list.size != 0)
365 update_active_sink_input.begin (_sink_input_list.get (0));
366 else
367 update_active_sink_input.begin (-1);
368 }
369 }
370 }
371
372 private void handle_new_sink_input_cb (Context c, SinkInputInfo? i, int eol)
373 {
374 if (i == null)
375 return;
376
377 add_sink_input_into_list (i);
378 }
379
380 private void handle_changed_sink_input_cb (Context c, SinkInputInfo? i, int eol)
381 {
382 if (i == null)
383 return;
384
385 if (i.index in _sink_input_list) {
386 /* Phone stream is always corked, so handle it differently */
387 if (i.corked == 1 && _sink_input_hash.get (i.index) != _objp_role_phone)
388 remove_sink_input_from_list (i.index);
389 } else {
390 if (i.corked == 0)
391 add_sink_input_into_list (i);
392 }
393 }
394
395 private void source_output_info_cb (Context c, SourceOutputInfo? i, int eol)
396 {
397 if (i == null)
398 return;
399
400 var role = i.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
401 if (role == "phone" || role == "production")
402 this.active_mic = true;
403 }322 }
404323
405 private void context_state_callback (Context c)324 private void context_state_callback (Context c)
406 {325 {
407 switch (c.get_state ()) {326 switch (c.get_state ()) {
408 case Context.State.READY:327 case Context.State.READY:
409 if (_pulse_use_stream_restore) {328 PulseAudio.Extension.StreamRestore.test(this.context, (context, version) => {
410 c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |329 if (version != 0) {
411 PulseAudio.Context.SubscriptionMask.SINK_INPUT |330 debug("Got the stream restore extension");
412 PulseAudio.Context.SubscriptionMask.SOURCE |331 _pulse_use_stream_restore = true;
413 PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);332 } else {
414 } else {333 debug("No stream restore extension found");
415 c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |334 }
416 PulseAudio.Context.SubscriptionMask.SOURCE |335
417 PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);336 if (_pulse_use_stream_restore) {
418 }337 this.context.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
419 c.set_subscribe_callback (context_events_cb);338 PulseAudio.Context.SubscriptionMask.SINK_INPUT |
420 update_sink ();339 PulseAudio.Context.SubscriptionMask.SOURCE |
421 update_source ();340 PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
422 this.ready = true;341 } else {
342 this.context.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
343 PulseAudio.Context.SubscriptionMask.SOURCE |
344 PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
345 }
346 this.context.set_subscribe_callback (context_events_cb);
347 this.ready = true;
348
349 debug("Pulse state ready");
350
351 update_sink ();
352 update_sink_input ();
353 update_source ();
354 update_stream ();
355 });
356
423 break;357 break;
424358
425 case Context.State.FAILED:359 case Context.State.FAILED:
426 case Context.State.TERMINATED:360 case Context.State.TERMINATED:
361 warning("Pulse state disconnected, retrying.");
362 this.ready = false;
427 if (_reconnect_timer == 0)363 if (_reconnect_timer == 0)
428 _reconnect_timer = Timeout.add_seconds (2, reconnect_timeout);364 _reconnect_timer = Timeout.add_seconds (2, () => {
365 _reconnect_timer = 0;
366 reconnect_to_pulse ();
367 return false; // G_SOURCE_REMOVE
368 });
369 break;
370
371 case Context.State.CONNECTING:
372 case Context.State.AUTHORIZING:
373 case Context.State.SETTING_NAME:
374 debug(@"Pulse state initializing step $((int)c.get_state())");
429 break;375 break;
430376
431 default:377 default:
432 this.ready = false;378 warning("Unknown state returned by Pulse Audio context. Not reconnecting.");
433 break;379 break;
434 }380 }
435 }381 }
436382
437 bool reconnect_timeout ()
438 {
439 _reconnect_timer = 0;
440 reconnect_to_pulse ();
441 return false; // G_SOURCE_REMOVE
442 }
443
444 void reconnect_to_pulse ()383 void reconnect_to_pulse ()
445 {384 {
446 if (this.ready) {385 if (this.ready) {
447 this.context.disconnect ();386 this.context.disconnect ();
448 this.context = null;387 this.context = null;
449 this.ready = false;388 this.ready = false;
389 this._pulse_use_stream_restore = false;
450 }390 }
451391
452 var props = new Proplist ();392 var props = new Proplist ();
@@ -455,8 +395,6 @@
455 props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control");395 props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control");
456 props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1");396 props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1");
457397
458 reconnect_pulse_dbus ();
459
460 this.context = new PulseAudio.Context (loop.get_api(), null, props);398 this.context = new PulseAudio.Context (loop.get_api(), null, props);
461 this.context.set_state_callback (context_state_callback);399 this.context.set_state_callback (context_state_callback);
462400
@@ -464,60 +402,54 @@
464 warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno()));402 warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno()));
465 }403 }
466404
467 void sink_info_list_callback_set_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) {405 /******************************/
468 if (sink != null)406 /* Mute operations */
469 context.set_sink_mute_by_index (sink.index, true, null);407 /******************************/
470 }408
471
472 void sink_info_list_callback_unset_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) {
473 if (sink != null)
474 context.set_sink_mute_by_index (sink.index, false, null);
475 }
476
477 /* Mute operations */
478 bool set_mute_internal (bool mute)409 bool set_mute_internal (bool mute)
479 {410 {
480 return_val_if_fail (context.get_state () == Context.State.READY, false);411 if (!this.ready)
412 return false;
481413
482 if (_mute != mute) {414 if (_mute != mute) {
483 if (mute)415 if (mute)
484 context.get_sink_info_list (sink_info_list_callback_set_mute);416 context.get_sink_info_list ((context, sink, eol) => {
417 if (sink != null)
418 context.set_sink_mute_by_index (sink.index, true, null);
419 });
485 else420 else
486 context.get_sink_info_list (sink_info_list_callback_unset_mute);421 context.get_sink_info_list ((context, sink, eol) => {
422 if (sink != null)
423 context.set_sink_mute_by_index (sink.index, false, null);
424 });
487 return true;425 return true;
488 } else {426 } else {
489 return false;427 return false;
490 }428 }
491 }429 }
492430
493 public void set_mute (bool mute)
494 {
495 if (set_mute_internal (mute))
496 sync_mute_to_accountsservice.begin (mute);
497 }
498
499 public void toggle_mute ()431 public void toggle_mute ()
500 {432 {
501 this.set_mute (!this._mute);433 this.mute = !this._mute;
502 }434 }
503435
504 public bool mute436 public override bool mute
505 {437 {
506 get438 get
507 {439 {
508 return this._mute;440 return this._mute;
509 }441 }
510 }442 set
511
512 public bool is_playing
513 {
514 get
515 {443 {
516 return this._is_playing;444 if (set_mute_internal (value))
445 sync_mute_to_accountsservice.begin (value);
517 }446 }
518 }447 }
519448
520 /* Volume operations */449 /******************************/
450 /* Volume operations */
451 /******************************/
452
521 private static PulseAudio.Volume double_to_volume (double vol)453 private static PulseAudio.Volume double_to_volume (double vol)
522 {454 {
523 double tmp = (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED) * vol;455 double tmp = (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED) * vol;
@@ -530,79 +462,41 @@
530 return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED);462 return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED);
531 }463 }
532464
533 private void set_volume_success_cb (Context c, int success)465 bool set_volume_internal (double newvolume)
534 {466 {
535 if ((bool)success)467 if (!this.ready)
536 this.notify_property("volume");
537 }
538
539 private void sink_info_set_volume_cb (Context c, SinkInfo? i, int eol)
540 {
541 if (i == null)
542 return;
543
544 unowned CVolume cvol = i.volume;
545 cvol.scale (double_to_volume (_volume));
546 c.set_sink_volume_by_index (i.index, cvol, set_volume_success_cb);
547 }
548
549 private void server_info_cb_for_set_volume (Context c, ServerInfo? i)
550 {
551 if (i == null)
552 {
553 warning ("Could not get PulseAudio server info");
554 return;
555 }
556
557 context.get_sink_info_by_name (i.default_sink_name, sink_info_set_volume_cb);
558 }
559
560 private async void set_volume_active_role ()
561 {
562 string active_role_objp = _objp_role_alert;
563
564 if (_active_sink_input != -1 && _active_sink_input in _sink_input_list)
565 active_role_objp = _sink_input_hash.get (_active_sink_input);
566
567 try {
568 var builder = new VariantBuilder (new VariantType ("a(uu)"));
569 builder.add ("(uu)", 0, double_to_volume (_volume));
570 Variant volume = builder.end ();
571
572 /* Increase the signal counter so we can handle the callback */
573 lock (_pa_volume_sig_count) {
574 _pa_volume_sig_count++;
575 }
576
577 yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry",
578 active_role_objp, "org.freedesktop.DBus.Properties", "Set",
579 new Variant ("(ssv)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume", volume),
580 null, DBusCallFlags.NONE, -1);
581
582 this.notify_property("volume");
583 } catch (GLib.Error e) {
584 lock (_pa_volume_sig_count) {
585 _pa_volume_sig_count--;
586 }
587 warning ("unable to set volume for stream obj path %s (%s)", active_role_objp, e.message);
588 }
589 }
590
591 bool set_volume_internal (double volume)
592 {
593 if (context.get_state () != Context.State.READY)
594 return false;468 return false;
595469
596 if (_volume != volume) {470 if (_volume != newvolume) {
597 var old_high_volume = this.high_volume;471 var old_high_volume = this.high_volume;
598472 /* Ideally, we'd be able to set this when we give the value to Pulse, but
599 _volume = volume;473 we kinda have a Vala bug that's messing up capturing the variables. So
600 if (_pulse_use_stream_restore)474 we're setting it here so that the VolumeControl object can track it all
601 set_volume_active_role.begin ();475 the way through */
602 else476 /* https://bugzilla.gnome.org/show_bug.cgi?id=741485 */
603 context.get_server_info (server_info_cb_for_set_volume);477 _volume = newvolume;
604478
605 this.notify_property("volume");479 context.get_server_info ((context, i) => {
480 if (i == null)
481 {
482 warning ("Could not get PulseAudio server info");
483 return;
484 }
485
486 context.get_sink_info_by_name (i.default_sink_name, (context, info, eol) => {
487 if (info == null)
488 return;
489
490 unowned CVolume cvol = info.volume;
491 cvol.scale (double_to_volume (this._volume));
492 context.set_sink_volume_by_index (info.index, cvol, (context, success) => {
493 if (!(bool)success)
494 return;
495
496 this.notify_property("volume");
497 });
498 });
499 });
606500
607 if (this.high_volume != old_high_volume)501 if (this.high_volume != old_high_volume)
608 this.notify_property("high-volume");502 this.notify_property("high-volume");
@@ -613,21 +507,7 @@
613 }507 }
614 }508 }
615509
616 void set_mic_volume_success_cb (Context c, int success)510 public override double volume {
617 {
618 if ((bool)success)
619 this.notify_property ("mic-volume");
620 }
621
622 void set_mic_volume_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) {
623 if (i != null) {
624 unowned CVolume cvol = CVolume ();
625 cvol = vol_set (cvol, 1, double_to_volume (_mic_volume));
626 c.set_source_volume_by_name (i.default_source_name, cvol, set_mic_volume_success_cb);
627 }
628 }
629
630 public double volume {
631 get {511 get {
632 return _volume;512 return _volume;
633 }513 }
@@ -638,99 +518,39 @@
638 }518 }
639 }519 }
640520
641 public double mic_volume {521 public override double mic_volume {
642 get {522 get {
643 return _mic_volume;523 return _mic_volume;
644 }524 }
645 set {525 set {
646 return_if_fail (context.get_state () == Context.State.READY);526 if (!this.ready)
527 return;
647528
529 /* Ideally, we'd be able to set this when we give the value to Pulse, but
530 we kinda have a Vala bug that's messing up capturing the variables. So
531 we're setting it here so that the VolumeControl object can track it all
532 the way through */
533 /* https://bugzilla.gnome.org/show_bug.cgi?id=741485 */
648 _mic_volume = value;534 _mic_volume = value;
649535
650 context.get_server_info (set_mic_volume_get_server_info_cb);536 context.get_server_info ((context, info) => {
651 }537 if (info == null)
652 }538 return;
653539
654 /* PulseAudio Dbus (Stream Restore) logic */540 unowned CVolume cvol = CVolume ();
655 private void reconnect_pulse_dbus ()541 cvol = vol_set (cvol, 1, double_to_volume (_mic_volume));
656 {542 context.set_source_volume_by_name (info.default_source_name, cvol, (context, success) => {
657 unowned string pulse_dbus_server_env = Environment.get_variable ("PULSE_DBUS_SERVER");543 if ((bool)success)
658 string address;544 this.notify_property ("mic-volume");
659545 });
660 /* In case of a reconnect */546 });
661 _pulse_use_stream_restore = false;547 }
662 _pa_volume_sig_count = 0;548 }
663549
664 if (pulse_dbus_server_env != null) {550 /******************************/
665 address = pulse_dbus_server_env;
666 } else {
667 DBusConnection conn;
668 Variant props;
669
670 try {
671 conn = Bus.get_sync (BusType.SESSION);
672 } catch (GLib.IOError e) {
673 warning ("unable to get the dbus session bus: %s", e.message);
674 return;
675 }
676
677 try {
678 var props_variant = conn.call_sync ("org.PulseAudio1",
679 "/org/pulseaudio/server_lookup1", "org.freedesktop.DBus.Properties",
680 "Get", new Variant ("(ss)", "org.PulseAudio.ServerLookup1", "Address"),
681 null, DBusCallFlags.NONE, -1);
682 props_variant.get ("(v)", out props);
683 address = props.get_string ();
684 } catch (GLib.Error e) {
685 warning ("unable to get pulse unix socket: %s", e.message);
686 return;
687 }
688 }
689
690 stdout.printf ("PulseAudio dbus unix socket: %s\n", address);
691 try {
692 _pconn = new DBusConnection.for_address_sync (address, DBusConnectionFlags.AUTHENTICATION_CLIENT);
693 } catch (GLib.Error e) {
694 /* If it fails, it means the dbus pulse extension is not available */
695 return;
696 }
697
698 /* For pulse dbus related events */
699 _pconn.add_filter (pulse_dbus_filter);
700
701 /* Check if the 4 currently supported media roles are already available in StreamRestore
702 * Roles: multimedia, alert, alarm and phone */
703 _objp_role_multimedia = stream_restore_get_object_path ("sink-input-by-media-role:multimedia");
704 _objp_role_alert = stream_restore_get_object_path ("sink-input-by-media-role:alert");
705 _objp_role_alarm = stream_restore_get_object_path ("sink-input-by-media-role:alarm");
706 _objp_role_phone = stream_restore_get_object_path ("sink-input-by-media-role:phone");
707
708 /* Only use stream restore if every used role is available */
709 if (_objp_role_multimedia != null && _objp_role_alert != null && _objp_role_alarm != null && _objp_role_phone != null) {
710 stdout.printf ("Using PulseAudio DBUS Stream Restore module\n");
711 /* Restore volume and update default entry */
712 update_active_sink_input.begin (-1);
713 _pulse_use_stream_restore = true;
714 }
715 }
716
717 private string? stream_restore_get_object_path (string name) {
718 string? objp = null;
719 try {
720 Variant props_variant = _pconn.call_sync ("org.PulseAudio.Ext.StreamRestore1",
721 "/org/pulseaudio/stream_restore1", "org.PulseAudio.Ext.StreamRestore1",
722 "GetEntryByName", new Variant ("(s)", name), null, DBusCallFlags.NONE, -1);
723 /* Workaround for older versions of vala that don't provide get_objv */
724 VariantIter iter = props_variant.iterator ();
725 iter.next ("o", &objp);
726 stdout.printf ("Found obj path %s for restore data named %s\n", objp, name);
727 } catch (GLib.Error e) {
728 warning ("unable to find stream restore data for: %s", name);
729 }
730 return objp;
731 }
732
733 /* AccountsService operations */551 /* AccountsService operations */
552 /******************************/
553
734 private void accountsservice_props_changed_cb (DBusProxy proxy, Variant changed_properties, string[]? invalidated_properties)554 private void accountsservice_props_changed_cb (DBusProxy proxy, Variant changed_properties, string[]? invalidated_properties)
735 {555 {
736 Variant volume_variant = changed_properties.lookup_value ("Volume", new VariantType ("d"));556 Variant volume_variant = changed_properties.lookup_value ("Volume", new VariantType ("d"));
737557
=== added file 'src/volume-control.vala'
--- src/volume-control.vala 1970-01-01 00:00:00 +0000
+++ src/volume-control.vala 2015-02-14 02:38:32 +0000
@@ -0,0 +1,31 @@
1/*
2 * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
3 * Copyright © 2015 Canonical Ltd.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors:
18 * Ted Gould <ted@canonical.com>
19 */
20
21public abstract class VolumeControl : Object
22{
23 public virtual string stream { get { return ""; } }
24 public virtual bool ready { get { return false; } set { } }
25 public virtual bool active_mic { get { return false; } set { } }
26 public virtual bool high_volume { get { return false; } }
27 public virtual bool mute { get { return false; } set { } }
28 public virtual bool is_playing { get { return false; } protected set { } }
29 public virtual double volume { get { return 0.0; } set { } }
30 public virtual double mic_volume { get { return 0.0; } set { } }
31}
032
=== modified file 'tests/CMakeLists.txt'
--- tests/CMakeLists.txt 2015-02-04 19:36:53 +0000
+++ tests/CMakeLists.txt 2015-02-14 02:38:32 +0000
@@ -64,6 +64,17 @@
64vala_add(vala-mocks64vala_add(vala-mocks
65 media-player-mock.vala65 media-player-mock.vala
66)66)
67vala_add(vala-mocks
68 focus-tracker-mock.vala
69)
70
71vala_add(vala-mocks
72 media-player-list-mock.vala
73)
74
75vala_add(vala-mocks
76 volume-control-mock.vala
77)
6778
68vala_finish(vala-mocks79vala_finish(vala-mocks
69 SOURCES80 SOURCES
@@ -160,6 +171,7 @@
160 volume-control-test171 volume-control-test
161 indicator-sound-service-lib172 indicator-sound-service-lib
162 pulse-mock173 pulse-mock
174 vala-mocks-lib
163 gtest175 gtest
164 ${TEST_LIBRARIES}176 ${TEST_LIBRARIES}
165)177)
@@ -184,6 +196,24 @@
184add_test(sound-menu-test sound-menu-test)196add_test(sound-menu-test sound-menu-test)
185197
186###########################198###########################
199# Notification Test
200###########################
201
202include_directories(${CMAKE_SOURCE_DIR}/src)
203add_executable (notifications-test notifications-test.cc)
204target_link_libraries (
205 notifications-test
206 indicator-sound-service-lib
207 vala-mocks-lib
208 pulse-mock
209 gtest
210 ${SOUNDSERVICE_LIBRARIES}
211 ${TEST_LIBRARIES}
212)
213
214add_test(notifications-test notifications-test)
215
216###########################
187# Accounts Service User217# Accounts Service User
188###########################218###########################
189219
190220
=== added file 'tests/focus-tracker-mock.vala'
--- tests/focus-tracker-mock.vala 1970-01-01 00:00:00 +0000
+++ tests/focus-tracker-mock.vala 2015-02-14 02:38:32 +0000
@@ -0,0 +1,27 @@
1/*
2 * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
3 * Copyright © 2015 Canonical Ltd.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors:
18 * Ted Gould <ted@canonical.com>
19 */
20
21public class FocusTrackerMock : FocusTracker {
22 public override string focused_appid { get; private set; default = "unknown"; }
23
24 public void mock_set_appid (string newappid) {
25 this.focused_appid = newappid;
26 }
27}
028
=== added file 'tests/gtest-gvariant.h'
--- tests/gtest-gvariant.h 1970-01-01 00:00:00 +0000
+++ tests/gtest-gvariant.h 2015-02-14 02:38:32 +0000
@@ -0,0 +1,110 @@
1/*
2 * Copyright © 2015 Canonical Ltd.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors:
17 * Ted Gould <ted@canonical.com>
18 */
19
20#include <gtest/gtest.h>
21#include <gio/gio.h>
22
23namespace GTestGVariant {
24
25testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, GVariant * expect, GVariant * have)
26{
27 if (expect == nullptr && have == nullptr) {
28 auto result = testing::AssertionSuccess();
29 return result;
30 }
31
32 if (expect == nullptr || have == nullptr) {
33 gchar * havePrint;
34 if (have == nullptr) {
35 havePrint = g_strdup("(nullptr)");
36 } else {
37 havePrint = g_variant_print(have, TRUE);
38 }
39
40 auto result = testing::AssertionFailure();
41 result <<
42 " Result: " << haveStr << std::endl <<
43 " Value: " << havePrint << std::endl <<
44 " Expected: " << expectStr << std::endl;
45
46 g_free(havePrint);
47 return result;
48 }
49
50 if (g_variant_equal(expect, have)) {
51 auto result = testing::AssertionSuccess();
52 return result;
53 } else {
54 gchar * havePrint = g_variant_print(have, TRUE);
55 gchar * expectPrint = g_variant_print(expect, TRUE);
56
57 auto result = testing::AssertionFailure();
58 result <<
59 " Result: " << haveStr << std::endl <<
60 " Value: " << havePrint << std::endl <<
61 " Expected: " << expectStr << std::endl <<
62 " Expected: " << expectPrint << std::endl;
63
64 g_free(havePrint);
65 g_free(expectPrint);
66
67 return result;
68 }
69}
70
71testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, std::shared_ptr<GVariant> expect, std::shared_ptr<GVariant> have)
72{
73 return expectVariantEqual(expectStr, haveStr, expect.get(), have.get());
74}
75
76testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, const char * expect, std::shared_ptr<GVariant> have)
77{
78 auto expectv = std::shared_ptr<GVariant>([expect] {
79 auto variant = g_variant_parse(nullptr, expect, nullptr, nullptr, nullptr);
80 if (variant != nullptr)
81 g_variant_ref_sink(variant);
82 return variant;
83 }(),
84 [](GVariant * variant) {
85 if (variant != nullptr)
86 g_variant_unref(variant);
87 });
88
89 return expectVariantEqual(expectStr, haveStr, expectv, have);
90}
91
92testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, const char * expect, GVariant * have)
93{
94 auto havep = std::shared_ptr<GVariant>([have] {
95 if (have != nullptr)
96 g_variant_ref_sink(have);
97 return have;
98 }(),
99 [](GVariant * variant) {
100 if (variant != nullptr)
101 g_variant_unref(variant);
102 });
103
104 return expectVariantEqual(expectStr, haveStr, expect, havep);
105}
106
107}; // ns GTestGVariant
108
109#define EXPECT_GVARIANT_EQ(expect, have) \
110 EXPECT_PRED_FORMAT2(GTestGVariant::expectVariantEqual, expect, have)
0111
=== modified file 'tests/indicator-test.cc'
--- tests/indicator-test.cc 2015-02-04 17:43:08 +0000
+++ tests/indicator-test.cc 2015-02-14 02:38:32 +0000
@@ -22,6 +22,7 @@
2222
23#include "indicator-fixture.h"23#include "indicator-fixture.h"
24#include "accounts-service-mock.h"24#include "accounts-service-mock.h"
25#include "notifications-mock.h"
2526
26class IndicatorTest : public IndicatorFixture27class IndicatorTest : public IndicatorFixture
27{28{
@@ -32,6 +33,7 @@
32 }33 }
3334
34 std::shared_ptr<AccountsServiceMock> as;35 std::shared_ptr<AccountsServiceMock> as;
36 std::shared_ptr<NotificationsMock> notification;
3537
36 virtual void SetUp() override38 virtual void SetUp() override
37 {39 {
@@ -45,12 +47,16 @@
45 as = std::make_shared<AccountsServiceMock>();47 as = std::make_shared<AccountsServiceMock>();
46 addMock(*as);48 addMock(*as);
4749
50 notification = std::make_shared<NotificationsMock>();
51 addMock(*notification);
52
48 IndicatorFixture::SetUp();53 IndicatorFixture::SetUp();
49 }54 }
5055
51 virtual void TearDown() override56 virtual void TearDown() override
52 {57 {
53 as.reset();58 as.reset();
59 notification.reset();
5460
55 IndicatorFixture::TearDown();61 IndicatorFixture::TearDown();
56 }62 }
5763
=== added file 'tests/media-player-list-mock.vala'
--- tests/media-player-list-mock.vala 1970-01-01 00:00:00 +0000
+++ tests/media-player-list-mock.vala 2015-02-14 02:38:32 +0000
@@ -0,0 +1,25 @@
1/*
2 * Copyright © 2014 Canonical Ltd.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors:
17 * Ted Gould <ted@canonical.com>
18 */
19
20public class MediaPlayerListMock : MediaPlayerList {
21 public override MediaPlayerList.Iterator iterator () { return new MediaPlayerList.Iterator(); }
22
23 public override void sync (string[] ids) { return; }
24}
25
026
=== added file 'tests/notifications-mock.h'
--- tests/notifications-mock.h 1970-01-01 00:00:00 +0000
+++ tests/notifications-mock.h 2015-02-14 02:38:32 +0000
@@ -0,0 +1,155 @@
1/*
2 * Copyright © 2015 Canonical Ltd.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors:
17 * Ted Gould <ted@canonical.com>
18 */
19
20#include <algorithm>
21#include <map>
22#include <memory>
23#include <type_traits>
24
25#include <libdbustest/dbus-test.h>
26
27class NotificationsMock
28{
29 DbusTestDbusMock * mock = nullptr;
30 DbusTestDbusMockObject * baseobj = nullptr;
31
32 public:
33 NotificationsMock (std::vector<std::string> capabilities = {"body", "body-markup", "icon-static", "image/svg+xml", "x-canonical-private-synchronous", "x-canonical-append", "x-canonical-private-icon-only", "x-canonical-truncation", "private-synchronous", "append", "private-icon-only", "truncation"}) {
34 mock = dbus_test_dbus_mock_new("org.freedesktop.Notifications");
35 dbus_test_task_set_bus(DBUS_TEST_TASK(mock), DBUS_TEST_SERVICE_BUS_SESSION);
36 dbus_test_task_set_name(DBUS_TEST_TASK(mock), "Notify");
37
38 baseobj =dbus_test_dbus_mock_get_object(mock, "/org/freedesktop/Notifications", "org.freedesktop.Notifications", nullptr);
39
40 std::string capspython("ret = ");
41 capspython += vector2py(capabilities);
42 dbus_test_dbus_mock_object_add_method(mock, baseobj,
43 "GetCapabilities", nullptr, G_VARIANT_TYPE("as"),
44 capspython.c_str(), nullptr);
45
46 dbus_test_dbus_mock_object_add_method(mock, baseobj,
47 "GetServerInformation", nullptr, G_VARIANT_TYPE("(ssss)"),
48 "ret = ['notification-mock', 'Testing harness', '1.0', '1.1']", nullptr);
49
50 dbus_test_dbus_mock_object_add_method(mock, baseobj,
51 "Notify", G_VARIANT_TYPE("(susssasa{sv}i)"), G_VARIANT_TYPE("u"),
52 "ret = 10", nullptr);
53
54 dbus_test_dbus_mock_object_add_method(mock, baseobj,
55 "CloseNotification", G_VARIANT_TYPE("u"), nullptr,
56 "", nullptr);
57 }
58
59 ~NotificationsMock () {
60 g_debug("Destroying the Notifications Mock");
61 g_clear_object(&mock);
62 }
63
64 std::string vector2py (std::vector<std::string> vect) {
65 std::string retval("[ ");
66
67 std::for_each(vect.begin(), vect.end() - 1, [&retval](std::string entry) {
68 retval += "'";
69 retval += entry;
70 retval += "', ";
71 });
72
73 retval += "'";
74 retval += *(vect.end() - 1);
75 retval += "']";
76
77 return retval;
78 }
79
80 operator std::shared_ptr<DbusTestTask> () {
81 std::shared_ptr<DbusTestTask> retval(DBUS_TEST_TASK(g_object_ref(mock)), [](DbusTestTask * task) { g_clear_object(&task); });
82 return retval;
83 }
84
85 operator DbusTestTask* () {
86 return DBUS_TEST_TASK(mock);
87 }
88
89 operator DbusTestDbusMock* () {
90 return mock;
91 }
92
93 struct Notification {
94 std::string app_name;
95 unsigned int replace_id;
96 std::string app_icon;
97 std::string summary;
98 std::string body;
99 std::vector<std::string> actions;
100 std::map<std::string, std::shared_ptr<GVariant>> hints;
101 int timeout;
102 };
103
104 std::shared_ptr<GVariant> childGet (GVariant * tuple, gsize index) {
105 return std::shared_ptr<GVariant>(g_variant_get_child_value(tuple, index),
106 [](GVariant * v){ if (v != nullptr) g_variant_unref(v); });
107 }
108
109 std::vector<Notification> getNotifications (void) {
110 std::vector<Notification> notifications;
111
112 unsigned int cnt, i;
113 auto calls = dbus_test_dbus_mock_object_get_method_calls(mock, baseobj, "Notify", &cnt, nullptr);
114
115 for (i = 0; i < cnt; i++) {
116 auto call = calls[i];
117 Notification notification;
118
119 notification.app_name = g_variant_get_string(childGet(call.params, 0).get(), nullptr);
120 notification.replace_id = g_variant_get_uint32(childGet(call.params, 1).get());
121 notification.app_icon = g_variant_get_string(childGet(call.params, 2).get(), nullptr);
122 notification.summary = g_variant_get_string(childGet(call.params, 3).get(), nullptr);
123 notification.body = g_variant_get_string(childGet(call.params, 4).get(), nullptr);
124 notification.timeout = g_variant_get_int32(childGet(call.params, 7).get());
125
126 auto vactions = childGet(call.params, 5);
127 GVariantIter iactions = {0};
128 g_variant_iter_init(&iactions, vactions.get());
129 const gchar * action = nullptr;
130 while (g_variant_iter_loop(&iactions, "&s", &action)) {
131 std::string saction(action);
132 notification.actions.push_back(saction);
133 }
134
135 auto vhints = childGet(call.params, 6);
136 GVariantIter ihints = {0};
137 g_variant_iter_init(&ihints, vhints.get());
138 const gchar * hint_key = nullptr;
139 GVariant * hint_value = nullptr;
140 while (g_variant_iter_loop(&ihints, "{&sv}", &hint_key, &hint_value)) {
141 std::string key(hint_key);
142 std::shared_ptr<GVariant> value(g_variant_ref(hint_value), [](GVariant * v){ if (v != nullptr) g_variant_unref(v); });
143 notification.hints[key] = value;
144 }
145
146 notifications.push_back(notification);
147 }
148
149 return notifications;
150 }
151
152 bool clearNotifications (void) {
153 return dbus_test_dbus_mock_object_clear_method_calls(mock, baseobj, nullptr);
154 }
155};
0156
=== added file 'tests/notifications-test.cc'
--- tests/notifications-test.cc 1970-01-01 00:00:00 +0000
+++ tests/notifications-test.cc 2015-02-14 02:38:32 +0000
@@ -0,0 +1,348 @@
1/*
2 * Copyright © 2015 Canonical Ltd.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors:
17 * Ted Gould <ted@canonical.com>
18 */
19
20#include <memory>
21
22#include <gtest/gtest.h>
23#include <gio/gio.h>
24#include <libdbustest/dbus-test.h>
25#include <libnotify/notify.h>
26
27#include "notifications-mock.h"
28#include "gtest-gvariant.h"
29
30extern "C" {
31#include "indicator-sound-service.h"
32#include "vala-mocks.h"
33}
34
35class NotificationsTest : public ::testing::Test
36{
37 protected:
38 DbusTestService * service = NULL;
39
40 GDBusConnection * session = NULL;
41 std::shared_ptr<NotificationsMock> notifications;
42
43 virtual void SetUp() {
44 g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, TRUE);
45 g_setenv("GSETTINGS_BACKEND", "memory", TRUE);
46
47 service = dbus_test_service_new(NULL);
48 dbus_test_service_set_bus(service, DBUS_TEST_SERVICE_BUS_SESSION);
49
50 /* Useful for debugging test failures, not needed all the time (until it fails) */
51 #if 0
52 auto bustle = std::shared_ptr<DbusTestTask>([]() {
53 DbusTestTask * bustle = DBUS_TEST_TASK(dbus_test_bustle_new("notifications-test.bustle"));
54 dbus_test_task_set_name(bustle, "Bustle");
55 dbus_test_task_set_bus(bustle, DBUS_TEST_SERVICE_BUS_SESSION);
56 return bustle;
57 }(), [](DbusTestTask * bustle) {
58 g_clear_object(&bustle);
59 });
60 dbus_test_service_add_task(service, bustle.get());
61 #endif
62
63 notifications = std::make_shared<NotificationsMock>();
64
65 dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
66 dbus_test_service_start_tasks(service);
67
68 session = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
69 ASSERT_NE(nullptr, session);
70 g_dbus_connection_set_exit_on_close(session, FALSE);
71 g_object_add_weak_pointer(G_OBJECT(session), (gpointer *)&session);
72
73 /* This is done in main.c */
74 notify_init("indicator-sound");
75 }
76
77 virtual void TearDown() {
78 if (notify_is_initted())
79 notify_uninit();
80
81 notifications.reset();
82 g_clear_object(&service);
83
84 g_object_unref(session);
85
86 unsigned int cleartry = 0;
87 while (session != NULL && cleartry < 100) {
88 loop(100);
89 cleartry++;
90 }
91
92 ASSERT_EQ(nullptr, session);
93 }
94
95 static gboolean timeout_cb (gpointer user_data) {
96 GMainLoop * loop = static_cast<GMainLoop *>(user_data);
97 g_main_loop_quit(loop);
98 return G_SOURCE_REMOVE;
99 }
100
101 void loop (unsigned int ms) {
102 GMainLoop * loop = g_main_loop_new(NULL, FALSE);
103 g_timeout_add(ms, timeout_cb, loop);
104 g_main_loop_run(loop);
105 g_main_loop_unref(loop);
106 }
107
108 static int unref_idle (gpointer user_data) {
109 g_variant_unref(static_cast<GVariant *>(user_data));
110 return G_SOURCE_REMOVE;
111 }
112
113 std::shared_ptr<MediaPlayerList> playerListMock () {
114 auto playerList = std::shared_ptr<MediaPlayerList>(
115 MEDIA_PLAYER_LIST(media_player_list_mock_new()),
116 [](MediaPlayerList * list) {
117 g_clear_object(&list);
118 });
119 return playerList;
120 }
121
122 std::shared_ptr<VolumeControl> volumeControlMock () {
123 auto volumeControl = std::shared_ptr<VolumeControl>(
124 VOLUME_CONTROL(volume_control_mock_new()),
125 [](VolumeControl * control){
126 g_clear_object(&control);
127 });
128 return volumeControl;
129 }
130
131 std::shared_ptr<IndicatorSoundService> standardService (std::shared_ptr<VolumeControl> volumeControl, std::shared_ptr<MediaPlayerList> playerList) {
132 auto soundService = std::shared_ptr<IndicatorSoundService>(
133 indicator_sound_service_new(playerList.get(), volumeControl.get(), nullptr),
134 [](IndicatorSoundService * service){
135 g_clear_object(&service);
136 });
137
138 return soundService;
139 }
140};
141
142TEST_F(NotificationsTest, BasicObject) {
143 auto soundService = standardService(volumeControlMock(), playerListMock());
144
145 /* Give some time settle */
146 loop(50);
147
148 /* Auto free */
149}
150
151TEST_F(NotificationsTest, VolumeChanges) {
152 auto volumeControl = volumeControlMock();
153 auto soundService = standardService(volumeControl, playerListMock());
154
155 /* Set a volume */
156 notifications->clearNotifications();
157 volume_control_set_volume(volumeControl.get(), 0.50);
158 loop(50);
159 auto notev = notifications->getNotifications();
160 ASSERT_EQ(1, notev.size());
161 EXPECT_EQ("indicator-sound", notev[0].app_name);
162 EXPECT_EQ("Volume", notev[0].summary);
163 EXPECT_EQ(0, notev[0].actions.size());
164 EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-private-synchronous"]);
165 EXPECT_GVARIANT_EQ("@i 50", notev[0].hints["value"]);
166
167 /* Set a different volume */
168 notifications->clearNotifications();
169 volume_control_set_volume(volumeControl.get(), 0.60);
170 loop(50);
171 notev = notifications->getNotifications();
172 ASSERT_EQ(1, notev.size());
173 EXPECT_GVARIANT_EQ("@i 60", notev[0].hints["value"]);
174
175 /* Set the same volume */
176 notifications->clearNotifications();
177 volume_control_set_volume(volumeControl.get(), 0.60);
178 loop(50);
179 notev = notifications->getNotifications();
180 ASSERT_EQ(0, notev.size());
181
182 /* Change just a little */
183 notifications->clearNotifications();
184 volume_control_set_volume(volumeControl.get(), 0.60001);
185 loop(50);
186 notev = notifications->getNotifications();
187 ASSERT_EQ(0, notev.size());
188}
189
190TEST_F(NotificationsTest, StreamChanges) {
191 auto volumeControl = volumeControlMock();
192 auto soundService = standardService(volumeControl, playerListMock());
193
194 /* Set a volume */
195 notifications->clearNotifications();
196 volume_control_set_volume(volumeControl.get(), 0.5);
197 loop(50);
198 auto notev = notifications->getNotifications();
199 ASSERT_EQ(1, notev.size());
200
201 /* Change Streams, no volume change */
202 notifications->clearNotifications();
203 volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "alarm");
204 volume_control_set_volume(volumeControl.get(), 0.5);
205 loop(50);
206 notev = notifications->getNotifications();
207 EXPECT_EQ(0, notev.size());
208
209 /* Change Streams, volume change */
210 notifications->clearNotifications();
211 volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "alert");
212 volume_control_set_volume(volumeControl.get(), 0.60);
213 loop(50);
214 notev = notifications->getNotifications();
215 EXPECT_EQ(0, notev.size());
216
217 /* Change Streams, no volume change, volume up */
218 notifications->clearNotifications();
219 volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "multimedia");
220 volume_control_set_volume(volumeControl.get(), 0.60);
221 loop(50);
222 volume_control_set_volume(volumeControl.get(), 0.65);
223 notev = notifications->getNotifications();
224 EXPECT_EQ(1, notev.size());
225 EXPECT_GVARIANT_EQ("@i 65", notev[0].hints["value"]);
226}
227
228TEST_F(NotificationsTest, IconTesting) {
229 auto volumeControl = volumeControlMock();
230 auto soundService = standardService(volumeControl, playerListMock());
231
232 /* Set an initial volume */
233 notifications->clearNotifications();
234 volume_control_set_volume(volumeControl.get(), 0.5);
235 loop(50);
236 auto notev = notifications->getNotifications();
237 ASSERT_EQ(1, notev.size());
238
239 /* Generate a set of notifications */
240 notifications->clearNotifications();
241 for (float i = 0.0; i < 1.01; i += 0.1) {
242 volume_control_set_volume(volumeControl.get(), i);
243 }
244
245 loop(50);
246 notev = notifications->getNotifications();
247 ASSERT_EQ(11, notev.size());
248
249 EXPECT_EQ("audio-volume-muted", notev[0].app_icon);
250 EXPECT_EQ("audio-volume-low", notev[1].app_icon);
251 EXPECT_EQ("audio-volume-low", notev[2].app_icon);
252 EXPECT_EQ("audio-volume-medium", notev[3].app_icon);
253 EXPECT_EQ("audio-volume-medium", notev[4].app_icon);
254 EXPECT_EQ("audio-volume-medium", notev[5].app_icon);
255 EXPECT_EQ("audio-volume-medium", notev[6].app_icon);
256 EXPECT_EQ("audio-volume-high", notev[7].app_icon);
257 EXPECT_EQ("audio-volume-high", notev[8].app_icon);
258 EXPECT_EQ("audio-volume-high", notev[9].app_icon);
259 EXPECT_EQ("audio-volume-high", notev[10].app_icon);
260}
261
262TEST_F(NotificationsTest, ServerRestart) {
263 auto volumeControl = volumeControlMock();
264 auto soundService = standardService(volumeControl, playerListMock());
265
266 /* Set a volume */
267 notifications->clearNotifications();
268 volume_control_set_volume(volumeControl.get(), 0.50);
269 loop(50);
270 auto notev = notifications->getNotifications();
271 ASSERT_EQ(1, notev.size());
272
273 /* Restart server without sync notifications */
274 notifications->clearNotifications();
275 dbus_test_service_remove_task(service, (DbusTestTask*)*notifications);
276 notifications.reset();
277
278 loop(50);
279
280 notifications = std::make_shared<NotificationsMock>(std::vector<std::string>({"body", "body-markup", "icon-static"}));
281 dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
282 dbus_test_task_run((DbusTestTask*)*notifications);
283
284 /* Change the volume */
285 notifications->clearNotifications();
286 volume_control_set_volume(volumeControl.get(), 0.60);
287 loop(50);
288 notev = notifications->getNotifications();
289 ASSERT_EQ(0, notev.size());
290
291 /* Put a good server back */
292 dbus_test_service_remove_task(service, (DbusTestTask*)*notifications);
293 notifications.reset();
294
295 loop(50);
296
297 notifications = std::make_shared<NotificationsMock>();
298 dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
299 dbus_test_task_run((DbusTestTask*)*notifications);
300
301 /* Change the volume again */
302 notifications->clearNotifications();
303 volume_control_set_volume(volumeControl.get(), 0.70);
304 loop(50);
305 notev = notifications->getNotifications();
306 ASSERT_EQ(1, notev.size());
307}
308
309TEST_F(NotificationsTest, HighVolume) {
310 auto volumeControl = volumeControlMock();
311 auto soundService = standardService(volumeControl, playerListMock());
312
313 /* Set a volume */
314 notifications->clearNotifications();
315 volume_control_set_volume(volumeControl.get(), 0.50);
316 loop(50);
317 auto notev = notifications->getNotifications();
318 ASSERT_EQ(1, notev.size());
319 EXPECT_EQ("Volume", notev[0].summary);
320 EXPECT_GVARIANT_EQ("@s 'false'", notev[0].hints["x-canonical-value-bar-tint"]);
321
322 /* Set high volume with volume change */
323 notifications->clearNotifications();
324 volume_control_mock_set_mock_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), TRUE);
325 volume_control_set_volume(volumeControl.get(), 0.90);
326 loop(50);
327 notev = notifications->getNotifications();
328 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 */
329 EXPECT_EQ("Volume", notev[0].summary);
330 EXPECT_EQ("High volume", notev[0].body);
331 EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-value-bar-tint"]);
332
333 /* Move it back */
334 volume_control_mock_set_mock_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), FALSE);
335 volume_control_set_volume(volumeControl.get(), 0.50);
336 loop(50);
337
338 /* Set high volume without level change */
339 /* NOTE: This can happen if headphones are plugged in */
340 notifications->clearNotifications();
341 volume_control_mock_set_mock_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), TRUE);
342 loop(50);
343 notev = notifications->getNotifications();
344 ASSERT_EQ(1, notev.size());
345 EXPECT_EQ("Volume", notev[0].summary);
346 EXPECT_EQ("High volume", notev[0].body);
347 EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-value-bar-tint"]);
348}
0349
=== modified file 'tests/pa-mock.cpp'
--- tests/pa-mock.cpp 2015-01-29 14:34:50 +0000
+++ tests/pa-mock.cpp 2015-02-14 02:38:32 +0000
@@ -23,6 +23,7 @@
2323
24#include <pulse/pulseaudio.h>24#include <pulse/pulseaudio.h>
25#include <pulse/glib-mainloop.h>25#include <pulse/glib-mainloop.h>
26#include <pulse/ext-stream-restore.h>
26#include <gio/gio.h>27#include <gio/gio.h>
27#include <math.h>28#include <math.h>
2829
@@ -280,6 +281,12 @@
280 return dummy_operation();281 return dummy_operation();
281}282}
282283
284pa_operation *
285pa_context_get_sink_input_info_list (pa_context *c, pa_sink_input_info_cb_t cb, void * userdata)
286{
287 return pa_context_get_sink_input_info (c, 0, cb, userdata);
288}
289
283pa_operation*290pa_operation*
284pa_context_get_source_info_by_name (pa_context *c, const char * name, pa_source_info_cb_t cb, void *userdata)291pa_context_get_source_info_by_name (pa_context *c, const char * name, pa_source_info_cb_t cb, void *userdata)
285{292{
@@ -569,3 +576,18 @@
569 return cvol;576 return cvol;
570}577}
571578
579/* *******************************
580 * ext-stream-restore.h
581 * *******************************/
582
583pa_operation *
584pa_ext_stream_restore_test (pa_context * c, pa_ext_stream_restore_test_cb_t callback, void * userdata)
585{
586 reinterpret_cast<PAMockContext*>(c)->idleOnce(
587 [c, callback, userdata]() {
588 if (callback != nullptr)
589 callback(c, 1, userdata);
590 });
591
592 return dummy_operation();
593}
572594
=== added file 'tests/volume-control-mock.vala'
--- tests/volume-control-mock.vala 1970-01-01 00:00:00 +0000
+++ tests/volume-control-mock.vala 2015-02-14 02:38:32 +0000
@@ -0,0 +1,43 @@
1/*
2 * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
3 * Copyright © 2015 Canonical Ltd.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors:
18 * Ted Gould <ted@canonical.com>
19 */
20
21public class VolumeControlMock : VolumeControl
22{
23 public string mock_stream { get; set; default = "multimedia"; }
24 public override string stream { get { return mock_stream; } }
25 public override bool ready { get; set; }
26 public override bool active_mic { get; set; }
27 public bool mock_high_volume { get; set; }
28 public override bool high_volume { get { return mock_high_volume; } }
29 public bool mock_mute { get; set; }
30 public override bool mute { get { return mock_mute; } set { } }
31 public bool mock_is_playing { get; set; }
32 public override bool is_playing { get { return mock_is_playing; } set { } }
33 public override double volume { get; set; }
34 public override double mic_volume { get; set; }
35
36 public VolumeControlMock() {
37 ready = true;
38 this.notify["mock-stream"].connect(() => this.notify_property("stream"));
39 this.notify["mock-high-volume"].connect(() => this.notify_property("high-volume"));
40 this.notify["mock-mute"].connect(() => this.notify_property("mute"));
41 this.notify["mock-is-playing"].connect(() => this.notify_property("is-playing"));
42 }
43}
044
=== modified file 'tests/volume-control-test.cc'
--- tests/volume-control-test.cc 2015-01-27 17:42:26 +0000
+++ tests/volume-control-test.cc 2015-02-14 02:38:32 +0000
@@ -23,6 +23,7 @@
2323
24extern "C" {24extern "C" {
25#include "indicator-sound-service.h"25#include "indicator-sound-service.h"
26#include "vala-mocks.h"
26}27}
2728
28class VolumeControlTest : public ::testing::Test29class VolumeControlTest : public ::testing::Test
@@ -71,13 +72,14 @@
71};72};
7273
73TEST_F(VolumeControlTest, BasicObject) {74TEST_F(VolumeControlTest, BasicObject) {
74 VolumeControl * control = volume_control_new();75 FocusTrackerMock * focustracker = focus_tracker_mock_new();
76 VolumeControlPulse * control = volume_control_pulse_new(FOCUS_TRACKER(focustracker));
7577
76 /* Setup the PA backend */78 /* Setup the PA backend */
77 loop(100);79 loop(100);
7880
79 /* Ready */81 /* Ready */
80 EXPECT_TRUE(volume_control_get_ready(control));82 EXPECT_TRUE(volume_control_get_ready(VOLUME_CONTROL(control)));
8183
82 g_clear_object(&control);84 g_clear_object(&control);
83}85}
8486
=== added file 'vapi/libpulse-ext-stream-restore.vapi'
--- vapi/libpulse-ext-stream-restore.vapi 1970-01-01 00:00:00 +0000
+++ vapi/libpulse-ext-stream-restore.vapi 2015-02-14 02:38:32 +0000
@@ -0,0 +1,22 @@
1
2using GLib;
3using PulseAudio;
4
5[CCode (cheader_filename="pulse/ext-stream-restore.h")]
6namespace PulseAudio.Extension.StreamRestore {
7 public struct Info {
8 string name;
9 PulseAudio.ChannelMap channel_map;
10 PulseAudio.CVolume volume;
11 string device;
12 int mute;
13 }
14
15 public delegate void TestCb (PulseAudio.Context context, uint32 version);
16 [CCode (cname="pa_ext_stream_restore_test")]
17 PulseAudio.Operation? test (PulseAudio.Context context, TestCb? callback = null);
18
19 public delegate void ReadCb (PulseAudio.Context context, PulseAudio.Extension.StreamRestore.Info[] streams, int eol);
20 [CCode (cname="pa_ext_stream_restore_read")]
21 PulseAudio.Operation? read (PulseAudio.Context context, ReadCb? callback = null);
22}

Subscribers

People subscribed via source and target branches