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
1=== modified file 'src/CMakeLists.txt'
2--- src/CMakeLists.txt 2015-01-27 17:42:26 +0000
3+++ src/CMakeLists.txt 2015-02-14 02:38:32 +0000
4@@ -36,6 +36,7 @@
5 --vapidir=.
6 --pkg=url-dispatcher
7 --pkg=bus-watcher
8+ --pkg=libpulse-ext-stream-restore
9 )
10
11 vala_add(indicator-sound-service
12@@ -43,15 +44,23 @@
13 DEPENDS
14 sound-menu
15 volume-control
16+ volume-control-pulse
17 media-player
18 media-player-list
19 mpris2-interfaces
20 accounts-service-user
21+ focus-tracker
22 )
23 vala_add(indicator-sound-service
24 volume-control.vala
25 )
26 vala_add(indicator-sound-service
27+ volume-control-pulse.vala
28+ DEPENDS
29+ volume-control
30+ focus-tracker
31+)
32+vala_add(indicator-sound-service
33 media-player.vala
34 )
35 vala_add(indicator-sound-service
36@@ -120,6 +129,14 @@
37 vala_add(indicator-sound-service
38 greeter-broadcast.vala
39 )
40+vala_add(indicator-sound-service
41+ focus-tracker.vala
42+)
43+vala_add(indicator-sound-service
44+ focus-tracker-stack.vala
45+ DEPENDS
46+ focus-tracker
47+)
48
49 vala_finish(indicator-sound-service
50 SOURCES
51@@ -154,6 +171,7 @@
52
53 add_definitions(
54 -w
55+ -DG_LOG_DOMAIN="indicator-sound"
56 )
57
58 add_library(
59
60=== added file 'src/focus-tracker-stack.vala'
61--- src/focus-tracker-stack.vala 1970-01-01 00:00:00 +0000
62+++ src/focus-tracker-stack.vala 2015-02-14 02:38:32 +0000
63@@ -0,0 +1,88 @@
64+/*
65+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
66+ * Copyright © 2014 Canonical Ltd.
67+ *
68+ * This program is free software; you can redistribute it and/or modify
69+ * it under the terms of the GNU General Public License as published by
70+ * the Free Software Foundation; version 3.
71+ *
72+ * This program is distributed in the hope that it will be useful,
73+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
74+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
75+ * GNU General Public License for more details.
76+ *
77+ * You should have received a copy of the GNU General Public License
78+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
79+ *
80+ * Authors:
81+ * Ted Gould <ted@canonical.com>
82+ */
83+
84+private struct WindowInfo {
85+ public uint window_id;
86+ public string app_id;
87+ public bool focused;
88+ public uint stage;
89+}
90+
91+/*
92+private struct DesktopInfo {
93+ public string app_id;
94+ public string desktop_file;
95+}
96+*/
97+
98+[DBus (name = "com.canonical.Unity.WindowStack")]
99+private interface WindowStack : Object {
100+ /* public abstract async DesktopInfo GetAppIdFromPid (uint pid) throws IOError; */
101+ public abstract async WindowInfo[] GetWindowStack () throws IOError;
102+ /* public abstract async string[] GetWindowProperties (uint window_id, string app_id, string[] names) throws IOError; */
103+
104+ public signal void FocusedWindowChanged (uint window_id, string app_id, uint stage);
105+ /* public signal void WindowCreated (uint window_id, string app_id); */
106+ /* public signal void WindowDestroyed (uint window_id, string app_id); */
107+}
108+
109+public class FocusTrackerStack : FocusTracker {
110+ public override string focused_appid { get; private set; default = "unknown"; }
111+ private WindowStack proxy;
112+
113+ public FocusTrackerStack ( ) {
114+ build_proxies.begin();
115+ }
116+
117+ private async void build_proxies() {
118+ try {
119+ proxy = yield Bus.get_proxy<WindowStack>(BusType.SESSION,
120+ "com.canonical.Unity.WindowStack",
121+ "/com/canonical/Unity/WindowStack",
122+ DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
123+ null);
124+
125+ proxy.FocusedWindowChanged.connect((window, appid, stage) => {
126+ if (stage == 0) {
127+ debug("Focus changed to: %s", appid);
128+ this.focused_appid = appid;
129+ }
130+ });
131+
132+ update_focused_appid.begin();
133+ } catch (Error e) {
134+ warning("Unable to create window stack proxy: %s", e.message);
135+ }
136+ }
137+
138+ private async void update_focused_appid () {
139+ try {
140+ var windows = yield proxy.GetWindowStack();
141+ foreach (var window in windows) {
142+ if (window.focused && window.stage == 0) {
143+ focused_appid = window.app_id;
144+ break;
145+ }
146+ }
147+ } catch (Error e) {
148+ warning("Unable to get window stack list: %s", e.message);
149+ }
150+ }
151+}
152
153=== added file 'src/focus-tracker.vala'
154--- src/focus-tracker.vala 1970-01-01 00:00:00 +0000
155+++ src/focus-tracker.vala 2015-02-14 02:38:32 +0000
156@@ -0,0 +1,22 @@
157+/*
158+ * Copyright © 2015 Canonical Ltd.
159+ *
160+ * This program is free software; you can redistribute it and/or modify
161+ * it under the terms of the GNU General Public License as published by
162+ * the Free Software Foundation; version 3.
163+ *
164+ * This program is distributed in the hope that it will be useful,
165+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
166+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
167+ * GNU General Public License for more details.
168+ *
169+ * You should have received a copy of the GNU General Public License
170+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
171+ *
172+ * Authors:
173+ * Ted Gould <ted@canonical.com>
174+ */
175+
176+public abstract class FocusTracker : Object {
177+ public virtual string focused_appid { get; protected set; }
178+}
179
180=== modified file 'src/main.c'
181--- src/main.c 2014-02-25 22:47:45 +0000
182+++ src/main.c 2015-02-14 02:38:32 +0000
183@@ -1,6 +1,21 @@
184-/* main.c generated by valac 0.22.1, the Vala compiler
185- * generated from main.vala, do not modify */
186-
187+/*
188+ * Copyright © 2015 Canonical Ltd.
189+ *
190+ * This program is free software; you can redistribute it and/or modify
191+ * it under the terms of the GNU General Public License as published by
192+ * the Free Software Foundation; version 3.
193+ *
194+ * This program is distributed in the hope that it will be useful,
195+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
196+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
197+ * GNU General Public License for more details.
198+ *
199+ * You should have received a copy of the GNU General Public License
200+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
201+ *
202+ * Authors:
203+ * Ted Gould <ted@canonical.com>
204+ */
205
206 #include <glib.h>
207 #include <locale.h>
208@@ -9,33 +24,79 @@
209 #include "indicator-sound-service.h"
210 #include "config.h"
211
212+static gboolean
213+sigterm_handler (gpointer data)
214+{
215+ g_debug("Got SIGTERM");
216+ g_main_loop_quit((GMainLoop *)data);
217+ return G_SOURCE_REMOVE;
218+}
219+
220+static void
221+name_lost (GDBusConnection * connection, const gchar * name, gpointer user_data)
222+{
223+ g_debug("Name lost");
224+ g_main_loop_quit((GMainLoop *)user_data);
225+}
226+
227 int
228 main (int argc, char ** argv) {
229- gint result = 0;
230+ GMainLoop * loop = NULL;
231 IndicatorSoundService* service = NULL;
232+ GDBusConnection * bus = NULL;
233
234 bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
235 setlocale (LC_ALL, "");
236 bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
237
238+ /* Grab DBus */
239+ GError * error = NULL;
240+ bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
241+ if (error != NULL) {
242+ g_error("Unable to get session bus: %s", error->message);
243+ g_error_free(error);
244+ return -1;
245+ }
246+
247+ /* Build Mainloop */
248+ loop = g_main_loop_new(NULL, FALSE);
249+
250+ g_unix_signal_add(SIGTERM, sigterm_handler, loop);
251+
252 /* Initialize libnotify */
253 notify_init ("indicator-sound");
254
255 MediaPlayerList * playerlist = NULL;
256+ AccountsServiceUser * accounts = NULL;
257
258 if (g_strcmp0("lightdm", g_get_user_name()) == 0) {
259 playerlist = MEDIA_PLAYER_LIST(media_player_list_greeter_new());
260 } else {
261 playerlist = MEDIA_PLAYER_LIST(media_player_list_mpris_new());
262+ accounts = accounts_service_user_new();
263 }
264
265- service = indicator_sound_service_new (playerlist);
266- result = indicator_sound_service_run (service);
267-
268- g_object_unref(playerlist);
269- g_object_unref(service);
270-
271- return result;
272+ FocusTracker * tracker = FOCUS_TRACKER(focus_tracker_stack_new());
273+ VolumeControlPulse * volume = volume_control_pulse_new(tracker);
274+
275+ service = indicator_sound_service_new (playerlist, volume, accounts);
276+
277+ g_bus_own_name_on_connection(bus,
278+ "com.canonical.indicator.sound",
279+ G_BUS_NAME_OWNER_FLAGS_NONE,
280+ NULL, /* acquired */
281+ name_lost,
282+ loop,
283+ NULL);
284+
285+ g_main_loop_run(loop);
286+
287+ g_clear_object(&playerlist);
288+ g_clear_object(&tracker);
289+ g_clear_object(&accounts);
290+ g_clear_object(&service);
291+ g_clear_object(&bus);
292+
293+ return 0;
294 }
295
296-
297
298=== modified file 'src/media-player-list.vala'
299--- src/media-player-list.vala 2014-02-24 22:47:50 +0000
300+++ src/media-player-list.vala 2015-02-14 02:38:32 +0000
301@@ -17,7 +17,7 @@
302 * Ted Gould <ted@canonical.com>
303 */
304
305-public class MediaPlayerList {
306+public abstract class MediaPlayerList : Object {
307 public class Iterator {
308 public virtual MediaPlayer? next_value() {
309 return null;
310
311=== modified file 'src/service.vala'
312--- src/service.vala 2015-02-05 14:52:58 +0000
313+++ src/service.vala 2015-02-14 02:38:32 +0000
314@@ -18,13 +18,29 @@
315 */
316
317 public class IndicatorSound.Service: Object {
318- public Service (MediaPlayerList playerlist) {
319+ DBusConnection bus;
320+ DBusProxy notification_proxy;
321+
322+ public Service (MediaPlayerList playerlist, VolumeControl volume, AccountsServiceUser? accounts) {
323+ try {
324+ bus = Bus.get_sync(GLib.BusType.SESSION);
325+ } catch (GLib.Error e) {
326+ error("Unable to get DBus session bus: %s", e.message);
327+ }
328+
329 sync_notification = new Notify.Notification(_("Volume"), "", "audio-volume-muted");
330- this.notification_server_watch = GLib.Bus.watch_name(GLib.BusType.SESSION,
331- "org.freedesktop.Notifications",
332- GLib.BusNameWatcherFlags.NONE,
333- () => { check_sync_notification = false; },
334- () => { check_sync_notification = false; });
335+ try {
336+ this.notification_proxy = new DBusProxy.for_bus_sync(GLib.BusType.SESSION,
337+ DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | DBusProxyFlags.DO_NOT_CONNECT_SIGNALS | DBusProxyFlags.DO_NOT_AUTO_START,
338+ null, /* interface info */
339+ "org.freedesktop.Notifications",
340+ "/org/freedesktop/Notifications",
341+ "org.freedesktop.Notifications",
342+ null);
343+ this.notification_proxy.notify["g-name-owner"].connect ( () => { debug("Notifications name owner changed"); check_sync_notification = false; } );
344+ } catch (GLib.Error e) {
345+ error("Unable to build notification proxy: %s", e.message);
346+ }
347
348 this.settings = new Settings ("com.canonical.indicator.sound");
349 this.sharedsettings = new Settings ("com.ubuntu.sound");
350@@ -32,12 +48,12 @@
351 this.settings.bind ("visible", this, "visible", SettingsBindFlags.GET);
352 this.notify["visible"].connect ( () => this.update_root_icon () );
353
354- this.volume_control = new VolumeControl ();
355+ this.volume_control = volume;
356+ this.volume_control.bind_property ("media-playing", this, "player-playing", BindingFlags.SYNC_CREATE);
357
358+ this.accounts_service = accounts;
359 /* If we're on the greeter, don't export */
360- if (GLib.Environment.get_user_name() != "lightdm") {
361- this.accounts_service = new AccountsServiceUser();
362-
363+ if (this.accounts_service != null) {
364 this.accounts_service.notify["showDataOnGreeter"].connect(() => {
365 this.export_to_accounts_service = this.accounts_service.showDataOnGreeter;
366 eventually_update_player_actions();
367@@ -90,9 +106,27 @@
368 }
369 }
370 });
371+
372+ /* Everything is built, let's put it on the bus */
373+ try {
374+ export_actions = bus.export_action_group ("/com/canonical/indicator/sound", this.actions);
375+ } catch (Error e) {
376+ critical ("%s", e.message);
377+ }
378+
379+ this.menus.@foreach ( (profile, menu) => menu.export (bus, @"/com/canonical/indicator/sound/$profile"));
380 }
381
382 ~Service() {
383+ debug("Destroying Service Object");
384+
385+ clear_acts_player();
386+
387+ if (this.player_action_update_id > 0) {
388+ Source.remove (this.player_action_update_id);
389+ this.player_action_update_id = 0;
390+ }
391+
392 if (this.sound_was_blocked_timeout_id > 0) {
393 Source.remove (this.sound_was_blocked_timeout_id);
394 this.sound_was_blocked_timeout_id = 0;
395@@ -102,6 +136,11 @@
396 GLib.Bus.unwatch_name(this.notification_server_watch);
397 this.notification_server_watch = 0;
398 }
399+
400+ if (this.export_actions != 0) {
401+ bus.unexport_action_group(this.export_actions);
402+ this.export_actions = 0;
403+ }
404 }
405
406 bool greeter_show_track () {
407@@ -115,30 +154,6 @@
408 this.accounts_service.player = null;
409 }
410
411- public int run () {
412- if (this.loop != null) {
413- warning ("service is already running");
414- return 1;
415- }
416-
417- Bus.own_name (BusType.SESSION, "com.canonical.indicator.sound", BusNameOwnerFlags.NONE,
418- this.bus_acquired, null, this.name_lost);
419-
420- this.loop = new MainLoop (null, false);
421-
422- GLib.Unix.signal_add(GLib.ProcessSignal.TERM, () => {
423- debug("SIGTERM recieved, stopping our mainloop");
424- this.loop.quit();
425- return false;
426- });
427-
428- this.loop.run ();
429-
430- clear_acts_player();
431-
432- return 0;
433- }
434-
435 public bool visible { get; set; }
436
437 public bool allow_amplified_volume {
438@@ -171,13 +186,13 @@
439 { "indicator-shown", null, null, "@b false", null },
440 };
441
442- MainLoop loop;
443 SimpleActionGroup actions;
444 HashTable<string, SoundMenu> menus;
445 Settings settings;
446 Settings sharedsettings;
447 VolumeControl volume_control;
448 MediaPlayerList players;
449+ public bool player_playing { get; set; default = false; }
450 uint player_action_update_id;
451 bool mute_blocks_sound;
452 uint sound_was_blocked_timeout_id;
453@@ -275,6 +290,7 @@
454
455 void update_sync_notification () {
456 if (!check_sync_notification) {
457+ support_sync_notification = false;
458 List<string> caps = Notify.get_server_caps ();
459 if (caps.find_custom ("x-canonical-private-synchronous", strcmp) != null) {
460 support_sync_notification = true;
461@@ -285,27 +301,12 @@
462 if (!support_sync_notification)
463 return;
464
465- /* Update our volume and output */
466- var oldoutput = this.last_output_notification;
467- this.last_output_notification = this.volume_control.stream;
468-
469- var oldvolume = this.last_volume_notification;
470- this.last_volume_notification = volume_control.volume;
471-
472- /* Suppress notifications of volume changes if it is because the
473- output stream changed. */
474- if (oldoutput != this.last_output_notification)
475- return;
476- /* Supress updates that don't change the value */
477- if (GLib.Math.fabs(oldvolume - this.last_volume_notification) < 0.01)
478- return;
479-
480 var shown_action = actions.lookup_action ("indicator-shown") as SimpleAction;
481 if (shown_action != null && shown_action.get_state().get_boolean())
482 return;
483
484 /* Determine Label */
485- string volume_label = "";
486+ string volume_label = volume_control.stream; /* TODO: Undo this */
487 if (volume_control.high_volume)
488 volume_label = _("High volume");
489
490@@ -341,13 +342,14 @@
491 }
492 }
493
494+ SimpleAction silent_action;
495 Action create_silent_mode_action () {
496 bool silentNow = false;
497 if (this.accounts_service != null) {
498 silentNow = this.accounts_service.silentMode;
499 }
500
501- var silent_action = new SimpleAction.stateful ("silent-mode", null, new Variant.boolean (silentNow));
502+ silent_action = new SimpleAction.stateful ("silent-mode", null, new Variant.boolean (silentNow));
503
504 /* If we're not dealing with accounts service, we'll just always be out
505 of silent mode and that's cool. */
506@@ -371,15 +373,16 @@
507 return silent_action;
508 }
509
510+ SimpleAction mute_action;
511 Action create_mute_action () {
512- var mute_action = new SimpleAction.stateful ("mute", null, new Variant.boolean (this.volume_control.mute));
513+ mute_action = new SimpleAction.stateful ("mute", null, new Variant.boolean (this.volume_control.mute));
514
515 mute_action.activate.connect ( (action, param) => {
516 action.change_state (new Variant.boolean (!action.get_state ().get_boolean ()));
517 });
518
519 mute_action.change_state.connect ( (action, val) => {
520- volume_control.set_mute (val.get_boolean ());
521+ volume_control.mute = val.get_boolean ();
522 });
523
524 this.volume_control.notify["mute"].connect ( () => {
525@@ -415,6 +418,7 @@
526 return mute_action;
527 }
528
529+ SimpleAction volume_action;
530 Action create_volume_action () {
531 /* The action's state is between be in [0.0, 1.0] instead of [0.0,
532 * max_volume], so that we don't need to update the slider menu item
533@@ -425,7 +429,7 @@
534
535 double volume = this.volume_control.volume / this.max_volume;
536
537- var volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, new Variant.double (volume));
538+ volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, new Variant.double (volume));
539
540 volume_action.change_state.connect ( (action, val) => {
541 double v = val.get_double () * this.max_volume;
542@@ -440,12 +444,26 @@
543 });
544
545 this.volume_control.notify["volume"].connect (() => {
546- var vol_action = this.actions.lookup_action ("volume") as SimpleAction;
547-
548 /* Normalize volume, because the volume action's state is [0.0, 1.0], see create_volume_action() */
549- vol_action.set_state (new Variant.double (this.volume_control.volume / this.max_volume));
550+ volume_action.set_state (new Variant.double (this.volume_control.volume / this.max_volume));
551
552 this.update_root_icon ();
553+
554+ /* Update our volume and output */
555+ var oldoutput = this.last_output_notification;
556+ this.last_output_notification = this.volume_control.stream;
557+
558+ var oldvolume = this.last_volume_notification;
559+ this.last_volume_notification = volume_control.volume;
560+
561+ /* Suppress notifications of volume changes if it is because the
562+ output stream changed. */
563+ if (oldoutput != this.last_output_notification)
564+ return;
565+ /* Supress updates that don't change the value */
566+ if (GLib.Math.fabs(oldvolume - this.last_volume_notification) < 0.01)
567+ return;
568+
569 this.update_sync_notification ();
570 });
571
572@@ -454,24 +472,26 @@
573 return volume_action;
574 }
575
576+ SimpleAction mic_volume_action;
577 Action create_mic_volume_action () {
578- var volume_action = new SimpleAction.stateful ("mic-volume", null, new Variant.double (this.volume_control.mic_volume));
579+ mic_volume_action = new SimpleAction.stateful ("mic-volume", null, new Variant.double (this.volume_control.mic_volume));
580
581- volume_action.change_state.connect ( (action, val) => {
582+ mic_volume_action.change_state.connect ( (action, val) => {
583 volume_control.mic_volume = val.get_double ();
584 });
585
586 this.volume_control.notify["mic-volume"].connect ( () => {
587- volume_action.set_state (new Variant.double (this.volume_control.mic_volume));
588+ mic_volume_action.set_state (new Variant.double (this.volume_control.mic_volume));
589 });
590
591- this.volume_control.bind_property ("ready", volume_action, "enabled", BindingFlags.SYNC_CREATE);
592+ this.volume_control.bind_property ("ready", mic_volume_action, "enabled", BindingFlags.SYNC_CREATE);
593
594- return volume_action;
595+ return mic_volume_action;
596 }
597
598+ SimpleAction high_volume_action;
599 Action create_high_volume_action () {
600- var high_volume_action = new SimpleAction.stateful("high-volume", null, new Variant.boolean (this.volume_control.high_volume));
601+ high_volume_action = new SimpleAction.stateful("high-volume", null, new Variant.boolean (this.volume_control.high_volume));
602
603 this.volume_control.notify["high-volume"].connect( () => {
604 high_volume_action.set_state(new Variant.boolean (this.volume_control.high_volume));
605@@ -481,19 +501,7 @@
606 return high_volume_action;
607 }
608
609- void bus_acquired (DBusConnection connection, string name) {
610- try {
611- connection.export_action_group ("/com/canonical/indicator/sound", this.actions);
612- } catch (Error e) {
613- critical ("%s", e.message);
614- }
615-
616- this.menus.@foreach ( (profile, menu) => menu.export (connection, @"/com/canonical/indicator/sound/$profile"));
617- }
618-
619- void name_lost (DBusConnection connection, string name) {
620- this.loop.quit ();
621- }
622+ uint export_actions = 0;
623
624 Variant action_state_for_player (MediaPlayer player, bool show_track = true) {
625 var builder = new VariantBuilder (new VariantType ("a{sv}"));
626@@ -510,6 +518,7 @@
627
628 bool update_player_actions () {
629 bool clear_accounts_player = true;
630+ bool player_playing = false;
631
632 foreach (var player in this.players) {
633 SimpleAction? action = this.actions.lookup_action (player.id) as SimpleAction;
634@@ -529,11 +538,20 @@
635 accounts_service.player = player;
636 clear_accounts_player = false;
637 }
638+
639+ if (player.state == "Playing") {
640+ player_playing = true;
641+ }
642 }
643
644 if (clear_accounts_player)
645 clear_acts_player();
646
647+ if (this.player_playing != player_playing) {
648+ this.player_playing = player_playing;
649+ update_root_icon();
650+ }
651+
652 this.player_action_update_id = 0;
653 return false;
654 }
655
656=== modified file 'src/sound-menu.vala'
657--- src/sound-menu.vala 2015-01-29 17:31:03 +0000
658+++ src/sound-menu.vala 2015-02-14 02:38:32 +0000
659@@ -73,9 +73,20 @@
660 this.greeter_players = (flags & DisplayFlags.GREETER_PLAYERS) != 0;
661 }
662
663+ ~SoundMenu () {
664+ if (export_id != 0) {
665+ bus.unexport_menu_model(export_id);
666+ export_id = 0;
667+ }
668+ }
669+
670+ DBusConnection? bus = null;
671+ uint export_id = 0;
672+
673 public void export (DBusConnection connection, string object_path) {
674+ bus = connection;
675 try {
676- connection.export_menu_model (object_path, this.root);
677+ export_id = bus.export_menu_model (object_path, this.root);
678 } catch (Error e) {
679 critical ("%s", e.message);
680 }
681
682=== renamed file 'src/volume-control.vala' => 'src/volume-control-pulse.vala'
683--- src/volume-control.vala 2015-01-30 14:57:03 +0000
684+++ src/volume-control-pulse.vala 2015-02-14 02:38:32 +0000
685@@ -19,6 +19,7 @@
686 */
687
688 using PulseAudio;
689+using PulseAudio.Extension.StreamRestore;
690 using Notify;
691 using Gee;
692
693@@ -32,42 +33,51 @@
694 public signal void entry_selected (string entry_name);
695 }
696
697-public class VolumeControl : Object
698+public class VolumeControlPulse : VolumeControl
699 {
700 /* this is static to ensure it being freed after @context (loop does not have ref counting) */
701 private static PulseAudio.GLibMainLoop loop;
702
703+ private FocusTracker focus_tracker;
704+ public bool media_playing { get; set; default = false; }
705+
706 private uint _reconnect_timer = 0;
707
708 private PulseAudio.Context context;
709 private bool _mute = true;
710- private bool _is_playing = false;
711+ public override bool is_playing { get; private set; default = false; }
712 private double _volume = 0.0;
713 private double _mic_volume = 0.0;
714
715 /* Used by the pulseaudio stream restore extension */
716- private DBusConnection _pconn;
717- /* Need both the list and hash so we can retrieve the last known sink-input after
718- * releasing the current active one (restoring back to the previous known role) */
719- private Gee.ArrayList<uint32> _sink_input_list = new Gee.ArrayList<uint32> ();
720- private HashMap<uint32, string> _sink_input_hash = new HashMap<uint32, string> ();
721 private bool _pulse_use_stream_restore = false;
722- private uint32 _active_sink_input = -1;
723- private string[] _valid_roles = {"multimedia", "alert", "alarm", "phone"};
724- public string stream {
725+ private enum InputStreamType {
726+ ALERT = 0,
727+ ALARM = 1,
728+ MULTIMEDIA = 2,
729+ PHONE = 3
730+ }
731+ private InputStreamType currentstream = InputStreamType.ALERT;
732+ private InputStreamType tempstream = InputStreamType.ALERT;
733+ /** The name of the stream that we're currently playing */
734+ public override string stream {
735 get {
736- if (_active_sink_input < 0 || _active_sink_input >= _valid_roles.length)
737- return "multimedia";
738- else
739- return _valid_roles[_active_sink_input];
740+ switch (this.currentstream) {
741+ case InputStreamType.ALERT:
742+ return "alert";
743+ case InputStreamType.ALARM:
744+ return "alarm";
745+ case InputStreamType.MULTIMEDIA:
746+ return "multimedia";
747+ case InputStreamType.PHONE:
748+ return "phone";
749+ }
750+
751+ return "alert";
752 }
753 }
754- private string? _objp_role_multimedia = null;
755- private string? _objp_role_alert = null;
756- private string? _objp_role_alarm = null;
757- private string? _objp_role_phone = null;
758- private uint _pa_volume_sig_count = 0;
759
760+ /* Accounts Service Variables */
761 private DBusProxy _user_proxy;
762 private GreeterListInterface _greeter_proxy;
763 private Cancellable _mute_cancellable;
764@@ -76,23 +86,38 @@
765 private uint _accountservice_volume_timer = 0;
766 private bool _send_next_local_volume = false;
767 private double _account_service_volume = 0.0;
768+
769+
770 private bool _active_port_headphone = false;
771+ /* There is not easy way to check if the port is a headset/headphone besides
772+ * checking for the port name. On touch (with the pulseaudio droid element)
773+ * the headset/headphone port is called 'output-headset' and 'output-headphone'.
774+ * On the desktop this is usually called 'analog-output-headphones' */
775+ private string[] headphone_outputs = {"output-wired_headset", "output-wired_headphone", "analog-output-headphones"};
776
777 /** true when connected to the pulse server */
778- public bool ready { get; set; }
779+ public override bool ready { get; private set; }
780
781 /** true when a microphone is active **/
782- public bool active_mic { get; private set; default = false; }
783+ public override bool active_mic { get; private set; default = false; }
784
785 /** true when high volume warnings should be shown */
786- public bool high_volume {
787+ public override bool high_volume {
788 get {
789- return this._volume > 0.75 && _active_port_headphone;
790+ return this._volume > 0.75 && _active_port_headphone && stream == "multimedia";
791 }
792 }
793
794- public VolumeControl ()
795+ public VolumeControlPulse (FocusTracker tracker)
796 {
797+ this.focus_tracker = tracker;
798+ this.focus_tracker.notify["focused-appid"].connect(() => {
799+ if (!this.ready)
800+ return;
801+
802+ this.update_sink_input();
803+ });
804+
805 if (loop == null)
806 loop = new PulseAudio.GLibMainLoop ();
807
808@@ -102,9 +127,16 @@
809 setup_accountsservice.begin ();
810
811 this.reconnect_to_pulse ();
812+
813+ this.notify["media-playing"].connect(() => {
814+ update_sink_input();
815+ });
816+ this.notify["stream"].connect(() => {
817+ update_stream();
818+ });
819 }
820
821- ~VolumeControl ()
822+ ~VolumeControlPulse ()
823 {
824 if (_reconnect_timer != 0) {
825 Source.remove (_reconnect_timer);
826@@ -114,7 +146,10 @@
827 stop_account_service_volume_timer();
828 }
829
830- /* PulseAudio logic*/
831+ /************************/
832+ /* PulseAudio logic */
833+ /************************/
834+
835 private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index)
836 {
837 switch (t & Context.SubscriptionEventType.FACILITY_MASK)
838@@ -124,23 +159,7 @@
839 break;
840
841 case Context.SubscriptionEventType.SINK_INPUT:
842- switch (t & Context.SubscriptionEventType.TYPE_MASK)
843- {
844- case Context.SubscriptionEventType.NEW:
845- c.get_sink_input_info (index, handle_new_sink_input_cb);
846- break;
847-
848- case Context.SubscriptionEventType.CHANGE:
849- c.get_sink_input_info (index, handle_changed_sink_input_cb);
850- break;
851-
852- case Context.SubscriptionEventType.REMOVE:
853- remove_sink_input_from_list (index);
854- break;
855- default:
856- debug ("Sink input event not known.");
857- break;
858- }
859+ update_sink_input ();
860 break;
861
862 case Context.SubscriptionEventType.SOURCE:
863@@ -151,7 +170,14 @@
864 switch (t & Context.SubscriptionEventType.TYPE_MASK)
865 {
866 case Context.SubscriptionEventType.NEW:
867- c.get_source_output_info (index, source_output_info_cb);
868+ c.get_source_output_info (index, (context, info, eol) => {
869+ if (info == null)
870+ return;
871+
872+ var role = info.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
873+ if (role == "phone" || role == "production")
874+ this.active_mic = true;
875+ });
876 break;
877
878 case Context.SubscriptionEventType.REMOVE:
879@@ -162,291 +188,205 @@
880 }
881 }
882
883- private void sink_info_cb_for_props (Context c, SinkInfo? i, int eol)
884- {
885- bool old_high_volume = this.high_volume;
886-
887- if (i == null)
888- return;
889-
890- if (_mute != (bool)i.mute)
891- {
892- _mute = (bool)i.mute;
893- this.notify_property ("mute");
894- }
895-
896- var playing = (i.state == PulseAudio.SinkState.RUNNING);
897- if (_is_playing != playing)
898- {
899- _is_playing = playing;
900- this.notify_property ("is-playing");
901- }
902-
903- /* Check if the current active port is headset/headphone */
904- /* There is not easy way to check if the port is a headset/headphone besides
905- * checking for the port name. On touch (with the pulseaudio droid element)
906- * the headset/headphone port is called 'output-headset' and 'output-headphone'.
907- * On the desktop this is usually called 'analog-output-headphones' */
908- if (i.active_port != null &&
909- (i.active_port.name == "output-wired_headset" ||
910- i.active_port.name == "output-wired_headphone" ||
911- i.active_port.name == "analog-output-headphones")) {
912- _active_port_headphone = true;
913- } else {
914- _active_port_headphone = false;
915- }
916-
917- if (_pulse_use_stream_restore == false &&
918- _volume != volume_to_double (i.volume.max ()))
919- {
920- _volume = volume_to_double (i.volume.max ());
921- this.notify_property("volume");
922- start_local_volume_timer();
923- }
924-
925- if (this.high_volume != old_high_volume) {
926- this.notify_property("high-volume");
927- }
928- }
929-
930- private void source_info_cb (Context c, SourceInfo? i, int eol)
931- {
932- if (i == null)
933- return;
934-
935- if (_mic_volume != volume_to_double (i.volume.values[0]))
936- {
937- _mic_volume = volume_to_double (i.volume.values[0]);
938- this.notify_property ("mic-volume");
939- }
940- }
941-
942- private void server_info_cb_for_props (Context c, ServerInfo? i)
943- {
944- if (i == null)
945- return;
946- context.get_sink_info_by_name (i.default_sink_name, sink_info_cb_for_props);
947- }
948-
949 private void update_sink ()
950 {
951- context.get_server_info (server_info_cb_for_props);
952- }
953-
954- private void update_source_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) {
955- if (i != null)
956- context.get_source_info_by_name (i.default_source_name, source_info_cb);
957+ if (!this.ready)
958+ return;
959+
960+ context.get_server_info ((context, info) => {
961+ if (info == null)
962+ return;
963+
964+ context.get_sink_info_by_name (info.default_sink_name, (context, info, eol) => {
965+ bool old_high_volume = this.high_volume;
966+
967+ if (info == null)
968+ return;
969+
970+ if (_mute != (bool)info.mute)
971+ {
972+ _mute = (bool)info.mute;
973+ this.notify_property ("mute");
974+ }
975+
976+ this.is_playing = (info.state == PulseAudio.SinkState.RUNNING);
977+
978+ /* Check if the current active port is headset/headphone */
979+ if (info.active_port.name in headphone_outputs) {
980+ _active_port_headphone = true;
981+ } else {
982+ _active_port_headphone = false;
983+ }
984+
985+ if (_pulse_use_stream_restore == false &&
986+ _volume != volume_to_double (info.volume.max ()))
987+ {
988+ _volume = volume_to_double (info.volume.max ());
989+ this.notify_property("volume");
990+ start_local_volume_timer();
991+ }
992+
993+ if (this.high_volume != old_high_volume) {
994+ this.notify_property("high-volume");
995+ }
996+ });
997+ });
998+ }
999+
1000+ private void update_sink_input ()
1001+ {
1002+ if (!this.ready)
1003+ return;
1004+
1005+ if (!this._pulse_use_stream_restore)
1006+ return;
1007+
1008+ this.tempstream = InputStreamType.ALERT;
1009+ if (this.media_playing)
1010+ this.tempstream = InputStreamType.MULTIMEDIA;
1011+
1012+ context.get_sink_input_info_list((context, info, eol) => {
1013+ var inputstream = InputStreamType.ALARM;
1014+
1015+ if (info != null) {
1016+ var role = info.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
1017+ var corked = (info.corked != 0);
1018+ var appid = info.proplist.gets (PulseAudio.Proplist.PROP_APPLICATION_ID);
1019+
1020+ /* Override the corked value for the phone as it's always corked */
1021+ if (role != null && role == "phone")
1022+ corked = false;
1023+
1024+ if (!corked && role != null && appid == this.focus_tracker.focused_appid) {
1025+ switch (role) {
1026+ case "phone":
1027+ inputstream = InputStreamType.PHONE;
1028+ break;
1029+ case "alarm":
1030+ inputstream = InputStreamType.ALARM;
1031+ break;
1032+ case "alert":
1033+ inputstream = InputStreamType.ALERT;
1034+ break;
1035+ case "multimedia":
1036+ default:
1037+ /* We treat unknown roles (i.e. 'music') as multimedia */
1038+ inputstream = InputStreamType.MULTIMEDIA;
1039+ break;
1040+ }
1041+ }
1042+ }
1043+
1044+ if (inputstream > this.tempstream) {
1045+ this.tempstream = inputstream;
1046+ }
1047+
1048+ if (eol != 0) {
1049+ if (this.currentstream != this.tempstream) {
1050+ this.currentstream = this.tempstream;
1051+ debug("Current stream: %s", this.stream);
1052+ this.notify_property("stream");
1053+ }
1054+ }
1055+ });
1056+ }
1057+
1058+ /* Look at a given profile or role and figure out what our volume and
1059+ mute settings should be based on that. */
1060+ private void update_stream ()
1061+ {
1062+
1063+
1064 }
1065
1066 private void update_source ()
1067 {
1068- context.get_server_info (update_source_get_server_info_cb);
1069- }
1070-
1071- private DBusMessage pulse_dbus_filter (DBusConnection connection, owned DBusMessage message, bool incoming)
1072- {
1073- if (message.get_message_type () == DBusMessageType.SIGNAL) {
1074- string active_role_objp = _objp_role_alert;
1075- if (_active_sink_input != -1)
1076- active_role_objp = _sink_input_hash.get (_active_sink_input);
1077-
1078- if (message.get_path () == active_role_objp && message.get_member () == "VolumeUpdated") {
1079- uint sig_count = 0;
1080- lock (_pa_volume_sig_count) {
1081- sig_count = _pa_volume_sig_count;
1082- if (_pa_volume_sig_count > 0)
1083- _pa_volume_sig_count--;
1084- }
1085-
1086- /* We only care about signals if our internal count is zero */
1087- if (sig_count == 0) {
1088- /* Extract volume and make sure it's not a side effect of us setting it */
1089- Variant body = message.get_body ();
1090- Variant varray = body.get_child_value (0);
1091-
1092- uint32 type = 0, volume = 0;
1093- VariantIter iter = varray.iterator ();
1094- iter.next ("(uu)", &type, &volume);
1095- /* Here we need to compare integer values to avoid rounding issues, so just
1096- * using the volume values used by pulseaudio */
1097- PulseAudio.Volume cvolume = double_to_volume (_volume);
1098- if (volume != cvolume) {
1099- /* Someone else changed the volume for this role, reflect on the indicator */
1100- _volume = volume_to_double (volume);
1101- this.notify_property("volume");
1102- start_local_volume_timer();
1103- }
1104- }
1105- }
1106- }
1107-
1108- return message;
1109- }
1110-
1111- private async void update_active_sink_input (uint32 index)
1112- {
1113- if ((index == -1) || (index != _active_sink_input && index in _sink_input_list)) {
1114- string sink_input_objp = _objp_role_alert;
1115- if (index != -1)
1116- sink_input_objp = _sink_input_hash.get (index);
1117- _active_sink_input = index;
1118-
1119- /* Listen for role volume changes from pulse itself (external clients) */
1120- try {
1121- var builder = new VariantBuilder (new VariantType ("ao"));
1122- builder.add ("o", sink_input_objp);
1123-
1124- yield _pconn.call ("org.PulseAudio.Core1", "/org/pulseaudio/core1",
1125- "org.PulseAudio.Core1", "ListenForSignal",
1126- new Variant ("(sao)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry.VolumeUpdated", builder),
1127- null, DBusCallFlags.NONE, -1);
1128- } catch (GLib.Error e) {
1129- warning ("unable to listen for pulseaudio dbus signals (%s)", e.message);
1130- }
1131-
1132- try {
1133- var props_variant = yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry",
1134- sink_input_objp, "org.freedesktop.DBus.Properties", "Get",
1135- new Variant ("(ss)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume"),
1136- null, DBusCallFlags.NONE, -1);
1137- Variant tmp;
1138- props_variant.get ("(v)", out tmp);
1139- uint32 type = 0, volume = 0;
1140- VariantIter iter = tmp.iterator ();
1141- iter.next ("(uu)", &type, &volume);
1142-
1143- _volume = volume_to_double (volume);
1144- this.notify_property("volume");
1145- start_local_volume_timer();
1146- } catch (GLib.Error e) {
1147- warning ("unable to get volume for active role %s (%s)", sink_input_objp, e.message);
1148- }
1149- }
1150- }
1151-
1152- private void add_sink_input_into_list (SinkInputInfo sink_input)
1153- {
1154- /* We're only adding ones that are not corked and with a valid role */
1155- var role = sink_input.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
1156-
1157- if (role != null && role in _valid_roles) {
1158- if (sink_input.corked == 0 || role == "phone") {
1159- _sink_input_list.insert (0, sink_input.index);
1160- switch (role)
1161+ if (!this.ready)
1162+ return;
1163+
1164+ context.get_server_info ((context, info) => {
1165+ if (info == null)
1166+ return;
1167+
1168+ context.get_source_info_by_name (info.default_source_name, (context, info, eol) => {
1169+ if (info == null)
1170+ return;
1171+
1172+ if (_mic_volume != volume_to_double (info.volume.values[0]))
1173 {
1174- case "multimedia":
1175- _sink_input_hash.set (sink_input.index, _objp_role_multimedia);
1176- break;
1177- case "alert":
1178- _sink_input_hash.set (sink_input.index, _objp_role_alert);
1179- break;
1180- case "alarm":
1181- _sink_input_hash.set (sink_input.index, _objp_role_alarm);
1182- break;
1183- case "phone":
1184- _sink_input_hash.set (sink_input.index, _objp_role_phone);
1185- break;
1186+ _mic_volume = volume_to_double (info.volume.values[0]);
1187+ this.notify_property ("mic-volume");
1188 }
1189- /* Only switch the active sink input in case a phone one is not active */
1190- if (_active_sink_input == -1 ||
1191- _sink_input_hash.get (_active_sink_input) != _objp_role_phone)
1192- update_active_sink_input.begin (sink_input.index);
1193- }
1194- }
1195- }
1196-
1197- private void remove_sink_input_from_list (uint32 index)
1198- {
1199- if (index in _sink_input_list) {
1200- _sink_input_list.remove (index);
1201- _sink_input_hash.unset (index);
1202- if (index == _active_sink_input) {
1203- if (_sink_input_list.size != 0)
1204- update_active_sink_input.begin (_sink_input_list.get (0));
1205- else
1206- update_active_sink_input.begin (-1);
1207- }
1208- }
1209- }
1210-
1211- private void handle_new_sink_input_cb (Context c, SinkInputInfo? i, int eol)
1212- {
1213- if (i == null)
1214- return;
1215-
1216- add_sink_input_into_list (i);
1217- }
1218-
1219- private void handle_changed_sink_input_cb (Context c, SinkInputInfo? i, int eol)
1220- {
1221- if (i == null)
1222- return;
1223-
1224- if (i.index in _sink_input_list) {
1225- /* Phone stream is always corked, so handle it differently */
1226- if (i.corked == 1 && _sink_input_hash.get (i.index) != _objp_role_phone)
1227- remove_sink_input_from_list (i.index);
1228- } else {
1229- if (i.corked == 0)
1230- add_sink_input_into_list (i);
1231- }
1232- }
1233-
1234- private void source_output_info_cb (Context c, SourceOutputInfo? i, int eol)
1235- {
1236- if (i == null)
1237- return;
1238-
1239- var role = i.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
1240- if (role == "phone" || role == "production")
1241- this.active_mic = true;
1242+ });
1243+ });
1244 }
1245
1246 private void context_state_callback (Context c)
1247 {
1248 switch (c.get_state ()) {
1249 case Context.State.READY:
1250- if (_pulse_use_stream_restore) {
1251- c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
1252- PulseAudio.Context.SubscriptionMask.SINK_INPUT |
1253- PulseAudio.Context.SubscriptionMask.SOURCE |
1254- PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
1255- } else {
1256- c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
1257- PulseAudio.Context.SubscriptionMask.SOURCE |
1258- PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
1259- }
1260- c.set_subscribe_callback (context_events_cb);
1261- update_sink ();
1262- update_source ();
1263- this.ready = true;
1264+ PulseAudio.Extension.StreamRestore.test(this.context, (context, version) => {
1265+ if (version != 0) {
1266+ debug("Got the stream restore extension");
1267+ _pulse_use_stream_restore = true;
1268+ } else {
1269+ debug("No stream restore extension found");
1270+ }
1271+
1272+ if (_pulse_use_stream_restore) {
1273+ this.context.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
1274+ PulseAudio.Context.SubscriptionMask.SINK_INPUT |
1275+ PulseAudio.Context.SubscriptionMask.SOURCE |
1276+ PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
1277+ } else {
1278+ this.context.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
1279+ PulseAudio.Context.SubscriptionMask.SOURCE |
1280+ PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
1281+ }
1282+ this.context.set_subscribe_callback (context_events_cb);
1283+ this.ready = true;
1284+
1285+ debug("Pulse state ready");
1286+
1287+ update_sink ();
1288+ update_sink_input ();
1289+ update_source ();
1290+ update_stream ();
1291+ });
1292+
1293 break;
1294
1295 case Context.State.FAILED:
1296 case Context.State.TERMINATED:
1297+ warning("Pulse state disconnected, retrying.");
1298+ this.ready = false;
1299 if (_reconnect_timer == 0)
1300- _reconnect_timer = Timeout.add_seconds (2, reconnect_timeout);
1301+ _reconnect_timer = Timeout.add_seconds (2, () => {
1302+ _reconnect_timer = 0;
1303+ reconnect_to_pulse ();
1304+ return false; // G_SOURCE_REMOVE
1305+ });
1306+ break;
1307+
1308+ case Context.State.CONNECTING:
1309+ case Context.State.AUTHORIZING:
1310+ case Context.State.SETTING_NAME:
1311+ debug(@"Pulse state initializing step $((int)c.get_state())");
1312 break;
1313
1314 default:
1315- this.ready = false;
1316+ warning("Unknown state returned by Pulse Audio context. Not reconnecting.");
1317 break;
1318 }
1319 }
1320
1321- bool reconnect_timeout ()
1322- {
1323- _reconnect_timer = 0;
1324- reconnect_to_pulse ();
1325- return false; // G_SOURCE_REMOVE
1326- }
1327-
1328 void reconnect_to_pulse ()
1329 {
1330 if (this.ready) {
1331 this.context.disconnect ();
1332 this.context = null;
1333 this.ready = false;
1334+ this._pulse_use_stream_restore = false;
1335 }
1336
1337 var props = new Proplist ();
1338@@ -455,8 +395,6 @@
1339 props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control");
1340 props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1");
1341
1342- reconnect_pulse_dbus ();
1343-
1344 this.context = new PulseAudio.Context (loop.get_api(), null, props);
1345 this.context.set_state_callback (context_state_callback);
1346
1347@@ -464,60 +402,54 @@
1348 warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno()));
1349 }
1350
1351- void sink_info_list_callback_set_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) {
1352- if (sink != null)
1353- context.set_sink_mute_by_index (sink.index, true, null);
1354- }
1355-
1356- void sink_info_list_callback_unset_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) {
1357- if (sink != null)
1358- context.set_sink_mute_by_index (sink.index, false, null);
1359- }
1360-
1361- /* Mute operations */
1362+ /******************************/
1363+ /* Mute operations */
1364+ /******************************/
1365+
1366 bool set_mute_internal (bool mute)
1367 {
1368- return_val_if_fail (context.get_state () == Context.State.READY, false);
1369+ if (!this.ready)
1370+ return false;
1371
1372 if (_mute != mute) {
1373 if (mute)
1374- context.get_sink_info_list (sink_info_list_callback_set_mute);
1375+ context.get_sink_info_list ((context, sink, eol) => {
1376+ if (sink != null)
1377+ context.set_sink_mute_by_index (sink.index, true, null);
1378+ });
1379 else
1380- context.get_sink_info_list (sink_info_list_callback_unset_mute);
1381+ context.get_sink_info_list ((context, sink, eol) => {
1382+ if (sink != null)
1383+ context.set_sink_mute_by_index (sink.index, false, null);
1384+ });
1385 return true;
1386 } else {
1387 return false;
1388 }
1389 }
1390
1391- public void set_mute (bool mute)
1392- {
1393- if (set_mute_internal (mute))
1394- sync_mute_to_accountsservice.begin (mute);
1395- }
1396-
1397 public void toggle_mute ()
1398 {
1399- this.set_mute (!this._mute);
1400+ this.mute = !this._mute;
1401 }
1402
1403- public bool mute
1404+ public override bool mute
1405 {
1406 get
1407 {
1408 return this._mute;
1409 }
1410- }
1411-
1412- public bool is_playing
1413- {
1414- get
1415+ set
1416 {
1417- return this._is_playing;
1418+ if (set_mute_internal (value))
1419+ sync_mute_to_accountsservice.begin (value);
1420 }
1421 }
1422
1423- /* Volume operations */
1424+ /******************************/
1425+ /* Volume operations */
1426+ /******************************/
1427+
1428 private static PulseAudio.Volume double_to_volume (double vol)
1429 {
1430 double tmp = (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED) * vol;
1431@@ -530,79 +462,41 @@
1432 return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED);
1433 }
1434
1435- private void set_volume_success_cb (Context c, int success)
1436- {
1437- if ((bool)success)
1438- this.notify_property("volume");
1439- }
1440-
1441- private void sink_info_set_volume_cb (Context c, SinkInfo? i, int eol)
1442- {
1443- if (i == null)
1444- return;
1445-
1446- unowned CVolume cvol = i.volume;
1447- cvol.scale (double_to_volume (_volume));
1448- c.set_sink_volume_by_index (i.index, cvol, set_volume_success_cb);
1449- }
1450-
1451- private void server_info_cb_for_set_volume (Context c, ServerInfo? i)
1452- {
1453- if (i == null)
1454- {
1455- warning ("Could not get PulseAudio server info");
1456- return;
1457- }
1458-
1459- context.get_sink_info_by_name (i.default_sink_name, sink_info_set_volume_cb);
1460- }
1461-
1462- private async void set_volume_active_role ()
1463- {
1464- string active_role_objp = _objp_role_alert;
1465-
1466- if (_active_sink_input != -1 && _active_sink_input in _sink_input_list)
1467- active_role_objp = _sink_input_hash.get (_active_sink_input);
1468-
1469- try {
1470- var builder = new VariantBuilder (new VariantType ("a(uu)"));
1471- builder.add ("(uu)", 0, double_to_volume (_volume));
1472- Variant volume = builder.end ();
1473-
1474- /* Increase the signal counter so we can handle the callback */
1475- lock (_pa_volume_sig_count) {
1476- _pa_volume_sig_count++;
1477- }
1478-
1479- yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry",
1480- active_role_objp, "org.freedesktop.DBus.Properties", "Set",
1481- new Variant ("(ssv)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume", volume),
1482- null, DBusCallFlags.NONE, -1);
1483-
1484- this.notify_property("volume");
1485- } catch (GLib.Error e) {
1486- lock (_pa_volume_sig_count) {
1487- _pa_volume_sig_count--;
1488- }
1489- warning ("unable to set volume for stream obj path %s (%s)", active_role_objp, e.message);
1490- }
1491- }
1492-
1493- bool set_volume_internal (double volume)
1494- {
1495- if (context.get_state () != Context.State.READY)
1496+ bool set_volume_internal (double newvolume)
1497+ {
1498+ if (!this.ready)
1499 return false;
1500
1501- if (_volume != volume) {
1502+ if (_volume != newvolume) {
1503 var old_high_volume = this.high_volume;
1504-
1505- _volume = volume;
1506- if (_pulse_use_stream_restore)
1507- set_volume_active_role.begin ();
1508- else
1509- context.get_server_info (server_info_cb_for_set_volume);
1510-
1511- this.notify_property("volume");
1512+ /* Ideally, we'd be able to set this when we give the value to Pulse, but
1513+ we kinda have a Vala bug that's messing up capturing the variables. So
1514+ we're setting it here so that the VolumeControl object can track it all
1515+ the way through */
1516+ /* https://bugzilla.gnome.org/show_bug.cgi?id=741485 */
1517+ _volume = newvolume;
1518+
1519+ context.get_server_info ((context, i) => {
1520+ if (i == null)
1521+ {
1522+ warning ("Could not get PulseAudio server info");
1523+ return;
1524+ }
1525+
1526+ context.get_sink_info_by_name (i.default_sink_name, (context, info, eol) => {
1527+ if (info == null)
1528+ return;
1529+
1530+ unowned CVolume cvol = info.volume;
1531+ cvol.scale (double_to_volume (this._volume));
1532+ context.set_sink_volume_by_index (info.index, cvol, (context, success) => {
1533+ if (!(bool)success)
1534+ return;
1535+
1536+ this.notify_property("volume");
1537+ });
1538+ });
1539+ });
1540
1541 if (this.high_volume != old_high_volume)
1542 this.notify_property("high-volume");
1543@@ -613,21 +507,7 @@
1544 }
1545 }
1546
1547- void set_mic_volume_success_cb (Context c, int success)
1548- {
1549- if ((bool)success)
1550- this.notify_property ("mic-volume");
1551- }
1552-
1553- void set_mic_volume_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) {
1554- if (i != null) {
1555- unowned CVolume cvol = CVolume ();
1556- cvol = vol_set (cvol, 1, double_to_volume (_mic_volume));
1557- c.set_source_volume_by_name (i.default_source_name, cvol, set_mic_volume_success_cb);
1558- }
1559- }
1560-
1561- public double volume {
1562+ public override double volume {
1563 get {
1564 return _volume;
1565 }
1566@@ -638,99 +518,39 @@
1567 }
1568 }
1569
1570- public double mic_volume {
1571+ public override double mic_volume {
1572 get {
1573 return _mic_volume;
1574 }
1575 set {
1576- return_if_fail (context.get_state () == Context.State.READY);
1577+ if (!this.ready)
1578+ return;
1579
1580+ /* Ideally, we'd be able to set this when we give the value to Pulse, but
1581+ we kinda have a Vala bug that's messing up capturing the variables. So
1582+ we're setting it here so that the VolumeControl object can track it all
1583+ the way through */
1584+ /* https://bugzilla.gnome.org/show_bug.cgi?id=741485 */
1585 _mic_volume = value;
1586
1587- context.get_server_info (set_mic_volume_get_server_info_cb);
1588- }
1589- }
1590-
1591- /* PulseAudio Dbus (Stream Restore) logic */
1592- private void reconnect_pulse_dbus ()
1593- {
1594- unowned string pulse_dbus_server_env = Environment.get_variable ("PULSE_DBUS_SERVER");
1595- string address;
1596-
1597- /* In case of a reconnect */
1598- _pulse_use_stream_restore = false;
1599- _pa_volume_sig_count = 0;
1600-
1601- if (pulse_dbus_server_env != null) {
1602- address = pulse_dbus_server_env;
1603- } else {
1604- DBusConnection conn;
1605- Variant props;
1606-
1607- try {
1608- conn = Bus.get_sync (BusType.SESSION);
1609- } catch (GLib.IOError e) {
1610- warning ("unable to get the dbus session bus: %s", e.message);
1611- return;
1612- }
1613-
1614- try {
1615- var props_variant = conn.call_sync ("org.PulseAudio1",
1616- "/org/pulseaudio/server_lookup1", "org.freedesktop.DBus.Properties",
1617- "Get", new Variant ("(ss)", "org.PulseAudio.ServerLookup1", "Address"),
1618- null, DBusCallFlags.NONE, -1);
1619- props_variant.get ("(v)", out props);
1620- address = props.get_string ();
1621- } catch (GLib.Error e) {
1622- warning ("unable to get pulse unix socket: %s", e.message);
1623- return;
1624- }
1625- }
1626-
1627- stdout.printf ("PulseAudio dbus unix socket: %s\n", address);
1628- try {
1629- _pconn = new DBusConnection.for_address_sync (address, DBusConnectionFlags.AUTHENTICATION_CLIENT);
1630- } catch (GLib.Error e) {
1631- /* If it fails, it means the dbus pulse extension is not available */
1632- return;
1633- }
1634-
1635- /* For pulse dbus related events */
1636- _pconn.add_filter (pulse_dbus_filter);
1637-
1638- /* Check if the 4 currently supported media roles are already available in StreamRestore
1639- * Roles: multimedia, alert, alarm and phone */
1640- _objp_role_multimedia = stream_restore_get_object_path ("sink-input-by-media-role:multimedia");
1641- _objp_role_alert = stream_restore_get_object_path ("sink-input-by-media-role:alert");
1642- _objp_role_alarm = stream_restore_get_object_path ("sink-input-by-media-role:alarm");
1643- _objp_role_phone = stream_restore_get_object_path ("sink-input-by-media-role:phone");
1644-
1645- /* Only use stream restore if every used role is available */
1646- if (_objp_role_multimedia != null && _objp_role_alert != null && _objp_role_alarm != null && _objp_role_phone != null) {
1647- stdout.printf ("Using PulseAudio DBUS Stream Restore module\n");
1648- /* Restore volume and update default entry */
1649- update_active_sink_input.begin (-1);
1650- _pulse_use_stream_restore = true;
1651- }
1652- }
1653-
1654- private string? stream_restore_get_object_path (string name) {
1655- string? objp = null;
1656- try {
1657- Variant props_variant = _pconn.call_sync ("org.PulseAudio.Ext.StreamRestore1",
1658- "/org/pulseaudio/stream_restore1", "org.PulseAudio.Ext.StreamRestore1",
1659- "GetEntryByName", new Variant ("(s)", name), null, DBusCallFlags.NONE, -1);
1660- /* Workaround for older versions of vala that don't provide get_objv */
1661- VariantIter iter = props_variant.iterator ();
1662- iter.next ("o", &objp);
1663- stdout.printf ("Found obj path %s for restore data named %s\n", objp, name);
1664- } catch (GLib.Error e) {
1665- warning ("unable to find stream restore data for: %s", name);
1666- }
1667- return objp;
1668- }
1669-
1670+ context.get_server_info ((context, info) => {
1671+ if (info == null)
1672+ return;
1673+
1674+ unowned CVolume cvol = CVolume ();
1675+ cvol = vol_set (cvol, 1, double_to_volume (_mic_volume));
1676+ context.set_source_volume_by_name (info.default_source_name, cvol, (context, success) => {
1677+ if ((bool)success)
1678+ this.notify_property ("mic-volume");
1679+ });
1680+ });
1681+ }
1682+ }
1683+
1684+ /******************************/
1685 /* AccountsService operations */
1686+ /******************************/
1687+
1688 private void accountsservice_props_changed_cb (DBusProxy proxy, Variant changed_properties, string[]? invalidated_properties)
1689 {
1690 Variant volume_variant = changed_properties.lookup_value ("Volume", new VariantType ("d"));
1691
1692=== added file 'src/volume-control.vala'
1693--- src/volume-control.vala 1970-01-01 00:00:00 +0000
1694+++ src/volume-control.vala 2015-02-14 02:38:32 +0000
1695@@ -0,0 +1,31 @@
1696+/*
1697+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
1698+ * Copyright © 2015 Canonical Ltd.
1699+ *
1700+ * This program is free software; you can redistribute it and/or modify
1701+ * it under the terms of the GNU General Public License as published by
1702+ * the Free Software Foundation; version 3.
1703+ *
1704+ * This program is distributed in the hope that it will be useful,
1705+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1706+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1707+ * GNU General Public License for more details.
1708+ *
1709+ * You should have received a copy of the GNU General Public License
1710+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1711+ *
1712+ * Authors:
1713+ * Ted Gould <ted@canonical.com>
1714+ */
1715+
1716+public abstract class VolumeControl : Object
1717+{
1718+ public virtual string stream { get { return ""; } }
1719+ public virtual bool ready { get { return false; } set { } }
1720+ public virtual bool active_mic { get { return false; } set { } }
1721+ public virtual bool high_volume { get { return false; } }
1722+ public virtual bool mute { get { return false; } set { } }
1723+ public virtual bool is_playing { get { return false; } protected set { } }
1724+ public virtual double volume { get { return 0.0; } set { } }
1725+ public virtual double mic_volume { get { return 0.0; } set { } }
1726+}
1727
1728=== modified file 'tests/CMakeLists.txt'
1729--- tests/CMakeLists.txt 2015-02-04 19:36:53 +0000
1730+++ tests/CMakeLists.txt 2015-02-14 02:38:32 +0000
1731@@ -64,6 +64,17 @@
1732 vala_add(vala-mocks
1733 media-player-mock.vala
1734 )
1735+vala_add(vala-mocks
1736+ focus-tracker-mock.vala
1737+)
1738+
1739+vala_add(vala-mocks
1740+ media-player-list-mock.vala
1741+)
1742+
1743+vala_add(vala-mocks
1744+ volume-control-mock.vala
1745+)
1746
1747 vala_finish(vala-mocks
1748 SOURCES
1749@@ -160,6 +171,7 @@
1750 volume-control-test
1751 indicator-sound-service-lib
1752 pulse-mock
1753+ vala-mocks-lib
1754 gtest
1755 ${TEST_LIBRARIES}
1756 )
1757@@ -184,6 +196,24 @@
1758 add_test(sound-menu-test sound-menu-test)
1759
1760 ###########################
1761+# Notification Test
1762+###########################
1763+
1764+include_directories(${CMAKE_SOURCE_DIR}/src)
1765+add_executable (notifications-test notifications-test.cc)
1766+target_link_libraries (
1767+ notifications-test
1768+ indicator-sound-service-lib
1769+ vala-mocks-lib
1770+ pulse-mock
1771+ gtest
1772+ ${SOUNDSERVICE_LIBRARIES}
1773+ ${TEST_LIBRARIES}
1774+)
1775+
1776+add_test(notifications-test notifications-test)
1777+
1778+###########################
1779 # Accounts Service User
1780 ###########################
1781
1782
1783=== added file 'tests/focus-tracker-mock.vala'
1784--- tests/focus-tracker-mock.vala 1970-01-01 00:00:00 +0000
1785+++ tests/focus-tracker-mock.vala 2015-02-14 02:38:32 +0000
1786@@ -0,0 +1,27 @@
1787+/*
1788+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
1789+ * Copyright © 2015 Canonical Ltd.
1790+ *
1791+ * This program is free software; you can redistribute it and/or modify
1792+ * it under the terms of the GNU General Public License as published by
1793+ * the Free Software Foundation; version 3.
1794+ *
1795+ * This program is distributed in the hope that it will be useful,
1796+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1797+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1798+ * GNU General Public License for more details.
1799+ *
1800+ * You should have received a copy of the GNU General Public License
1801+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1802+ *
1803+ * Authors:
1804+ * Ted Gould <ted@canonical.com>
1805+ */
1806+
1807+public class FocusTrackerMock : FocusTracker {
1808+ public override string focused_appid { get; private set; default = "unknown"; }
1809+
1810+ public void mock_set_appid (string newappid) {
1811+ this.focused_appid = newappid;
1812+ }
1813+}
1814
1815=== added file 'tests/gtest-gvariant.h'
1816--- tests/gtest-gvariant.h 1970-01-01 00:00:00 +0000
1817+++ tests/gtest-gvariant.h 2015-02-14 02:38:32 +0000
1818@@ -0,0 +1,110 @@
1819+/*
1820+ * Copyright © 2015 Canonical Ltd.
1821+ *
1822+ * This program is free software; you can redistribute it and/or modify
1823+ * it under the terms of the GNU General Public License as published by
1824+ * the Free Software Foundation; version 3.
1825+ *
1826+ * This program is distributed in the hope that it will be useful,
1827+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1828+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1829+ * GNU General Public License for more details.
1830+ *
1831+ * You should have received a copy of the GNU General Public License
1832+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1833+ *
1834+ * Authors:
1835+ * Ted Gould <ted@canonical.com>
1836+ */
1837+
1838+#include <gtest/gtest.h>
1839+#include <gio/gio.h>
1840+
1841+namespace GTestGVariant {
1842+
1843+testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, GVariant * expect, GVariant * have)
1844+{
1845+ if (expect == nullptr && have == nullptr) {
1846+ auto result = testing::AssertionSuccess();
1847+ return result;
1848+ }
1849+
1850+ if (expect == nullptr || have == nullptr) {
1851+ gchar * havePrint;
1852+ if (have == nullptr) {
1853+ havePrint = g_strdup("(nullptr)");
1854+ } else {
1855+ havePrint = g_variant_print(have, TRUE);
1856+ }
1857+
1858+ auto result = testing::AssertionFailure();
1859+ result <<
1860+ " Result: " << haveStr << std::endl <<
1861+ " Value: " << havePrint << std::endl <<
1862+ " Expected: " << expectStr << std::endl;
1863+
1864+ g_free(havePrint);
1865+ return result;
1866+ }
1867+
1868+ if (g_variant_equal(expect, have)) {
1869+ auto result = testing::AssertionSuccess();
1870+ return result;
1871+ } else {
1872+ gchar * havePrint = g_variant_print(have, TRUE);
1873+ gchar * expectPrint = g_variant_print(expect, TRUE);
1874+
1875+ auto result = testing::AssertionFailure();
1876+ result <<
1877+ " Result: " << haveStr << std::endl <<
1878+ " Value: " << havePrint << std::endl <<
1879+ " Expected: " << expectStr << std::endl <<
1880+ " Expected: " << expectPrint << std::endl;
1881+
1882+ g_free(havePrint);
1883+ g_free(expectPrint);
1884+
1885+ return result;
1886+ }
1887+}
1888+
1889+testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, std::shared_ptr<GVariant> expect, std::shared_ptr<GVariant> have)
1890+{
1891+ return expectVariantEqual(expectStr, haveStr, expect.get(), have.get());
1892+}
1893+
1894+testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, const char * expect, std::shared_ptr<GVariant> have)
1895+{
1896+ auto expectv = std::shared_ptr<GVariant>([expect] {
1897+ auto variant = g_variant_parse(nullptr, expect, nullptr, nullptr, nullptr);
1898+ if (variant != nullptr)
1899+ g_variant_ref_sink(variant);
1900+ return variant;
1901+ }(),
1902+ [](GVariant * variant) {
1903+ if (variant != nullptr)
1904+ g_variant_unref(variant);
1905+ });
1906+
1907+ return expectVariantEqual(expectStr, haveStr, expectv, have);
1908+}
1909+
1910+testing::AssertionResult expectVariantEqual (const gchar * expectStr, const gchar * haveStr, const char * expect, GVariant * have)
1911+{
1912+ auto havep = std::shared_ptr<GVariant>([have] {
1913+ if (have != nullptr)
1914+ g_variant_ref_sink(have);
1915+ return have;
1916+ }(),
1917+ [](GVariant * variant) {
1918+ if (variant != nullptr)
1919+ g_variant_unref(variant);
1920+ });
1921+
1922+ return expectVariantEqual(expectStr, haveStr, expect, havep);
1923+}
1924+
1925+}; // ns GTestGVariant
1926+
1927+#define EXPECT_GVARIANT_EQ(expect, have) \
1928+ EXPECT_PRED_FORMAT2(GTestGVariant::expectVariantEqual, expect, have)
1929
1930=== modified file 'tests/indicator-test.cc'
1931--- tests/indicator-test.cc 2015-02-04 17:43:08 +0000
1932+++ tests/indicator-test.cc 2015-02-14 02:38:32 +0000
1933@@ -22,6 +22,7 @@
1934
1935 #include "indicator-fixture.h"
1936 #include "accounts-service-mock.h"
1937+#include "notifications-mock.h"
1938
1939 class IndicatorTest : public IndicatorFixture
1940 {
1941@@ -32,6 +33,7 @@
1942 }
1943
1944 std::shared_ptr<AccountsServiceMock> as;
1945+ std::shared_ptr<NotificationsMock> notification;
1946
1947 virtual void SetUp() override
1948 {
1949@@ -45,12 +47,16 @@
1950 as = std::make_shared<AccountsServiceMock>();
1951 addMock(*as);
1952
1953+ notification = std::make_shared<NotificationsMock>();
1954+ addMock(*notification);
1955+
1956 IndicatorFixture::SetUp();
1957 }
1958
1959 virtual void TearDown() override
1960 {
1961 as.reset();
1962+ notification.reset();
1963
1964 IndicatorFixture::TearDown();
1965 }
1966
1967=== added file 'tests/media-player-list-mock.vala'
1968--- tests/media-player-list-mock.vala 1970-01-01 00:00:00 +0000
1969+++ tests/media-player-list-mock.vala 2015-02-14 02:38:32 +0000
1970@@ -0,0 +1,25 @@
1971+/*
1972+ * Copyright © 2014 Canonical Ltd.
1973+ *
1974+ * This program is free software; you can redistribute it and/or modify
1975+ * it under the terms of the GNU General Public License as published by
1976+ * the Free Software Foundation; version 3.
1977+ *
1978+ * This program is distributed in the hope that it will be useful,
1979+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1980+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1981+ * GNU General Public License for more details.
1982+ *
1983+ * You should have received a copy of the GNU General Public License
1984+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1985+ *
1986+ * Authors:
1987+ * Ted Gould <ted@canonical.com>
1988+ */
1989+
1990+public class MediaPlayerListMock : MediaPlayerList {
1991+ public override MediaPlayerList.Iterator iterator () { return new MediaPlayerList.Iterator(); }
1992+
1993+ public override void sync (string[] ids) { return; }
1994+}
1995+
1996
1997=== added file 'tests/notifications-mock.h'
1998--- tests/notifications-mock.h 1970-01-01 00:00:00 +0000
1999+++ tests/notifications-mock.h 2015-02-14 02:38:32 +0000
2000@@ -0,0 +1,155 @@
2001+/*
2002+ * Copyright © 2015 Canonical Ltd.
2003+ *
2004+ * This program is free software; you can redistribute it and/or modify
2005+ * it under the terms of the GNU General Public License as published by
2006+ * the Free Software Foundation; version 3.
2007+ *
2008+ * This program is distributed in the hope that it will be useful,
2009+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2010+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2011+ * GNU General Public License for more details.
2012+ *
2013+ * You should have received a copy of the GNU General Public License
2014+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2015+ *
2016+ * Authors:
2017+ * Ted Gould <ted@canonical.com>
2018+ */
2019+
2020+#include <algorithm>
2021+#include <map>
2022+#include <memory>
2023+#include <type_traits>
2024+
2025+#include <libdbustest/dbus-test.h>
2026+
2027+class NotificationsMock
2028+{
2029+ DbusTestDbusMock * mock = nullptr;
2030+ DbusTestDbusMockObject * baseobj = nullptr;
2031+
2032+ public:
2033+ 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"}) {
2034+ mock = dbus_test_dbus_mock_new("org.freedesktop.Notifications");
2035+ dbus_test_task_set_bus(DBUS_TEST_TASK(mock), DBUS_TEST_SERVICE_BUS_SESSION);
2036+ dbus_test_task_set_name(DBUS_TEST_TASK(mock), "Notify");
2037+
2038+ baseobj =dbus_test_dbus_mock_get_object(mock, "/org/freedesktop/Notifications", "org.freedesktop.Notifications", nullptr);
2039+
2040+ std::string capspython("ret = ");
2041+ capspython += vector2py(capabilities);
2042+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
2043+ "GetCapabilities", nullptr, G_VARIANT_TYPE("as"),
2044+ capspython.c_str(), nullptr);
2045+
2046+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
2047+ "GetServerInformation", nullptr, G_VARIANT_TYPE("(ssss)"),
2048+ "ret = ['notification-mock', 'Testing harness', '1.0', '1.1']", nullptr);
2049+
2050+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
2051+ "Notify", G_VARIANT_TYPE("(susssasa{sv}i)"), G_VARIANT_TYPE("u"),
2052+ "ret = 10", nullptr);
2053+
2054+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
2055+ "CloseNotification", G_VARIANT_TYPE("u"), nullptr,
2056+ "", nullptr);
2057+ }
2058+
2059+ ~NotificationsMock () {
2060+ g_debug("Destroying the Notifications Mock");
2061+ g_clear_object(&mock);
2062+ }
2063+
2064+ std::string vector2py (std::vector<std::string> vect) {
2065+ std::string retval("[ ");
2066+
2067+ std::for_each(vect.begin(), vect.end() - 1, [&retval](std::string entry) {
2068+ retval += "'";
2069+ retval += entry;
2070+ retval += "', ";
2071+ });
2072+
2073+ retval += "'";
2074+ retval += *(vect.end() - 1);
2075+ retval += "']";
2076+
2077+ return retval;
2078+ }
2079+
2080+ operator std::shared_ptr<DbusTestTask> () {
2081+ std::shared_ptr<DbusTestTask> retval(DBUS_TEST_TASK(g_object_ref(mock)), [](DbusTestTask * task) { g_clear_object(&task); });
2082+ return retval;
2083+ }
2084+
2085+ operator DbusTestTask* () {
2086+ return DBUS_TEST_TASK(mock);
2087+ }
2088+
2089+ operator DbusTestDbusMock* () {
2090+ return mock;
2091+ }
2092+
2093+ struct Notification {
2094+ std::string app_name;
2095+ unsigned int replace_id;
2096+ std::string app_icon;
2097+ std::string summary;
2098+ std::string body;
2099+ std::vector<std::string> actions;
2100+ std::map<std::string, std::shared_ptr<GVariant>> hints;
2101+ int timeout;
2102+ };
2103+
2104+ std::shared_ptr<GVariant> childGet (GVariant * tuple, gsize index) {
2105+ return std::shared_ptr<GVariant>(g_variant_get_child_value(tuple, index),
2106+ [](GVariant * v){ if (v != nullptr) g_variant_unref(v); });
2107+ }
2108+
2109+ std::vector<Notification> getNotifications (void) {
2110+ std::vector<Notification> notifications;
2111+
2112+ unsigned int cnt, i;
2113+ auto calls = dbus_test_dbus_mock_object_get_method_calls(mock, baseobj, "Notify", &cnt, nullptr);
2114+
2115+ for (i = 0; i < cnt; i++) {
2116+ auto call = calls[i];
2117+ Notification notification;
2118+
2119+ notification.app_name = g_variant_get_string(childGet(call.params, 0).get(), nullptr);
2120+ notification.replace_id = g_variant_get_uint32(childGet(call.params, 1).get());
2121+ notification.app_icon = g_variant_get_string(childGet(call.params, 2).get(), nullptr);
2122+ notification.summary = g_variant_get_string(childGet(call.params, 3).get(), nullptr);
2123+ notification.body = g_variant_get_string(childGet(call.params, 4).get(), nullptr);
2124+ notification.timeout = g_variant_get_int32(childGet(call.params, 7).get());
2125+
2126+ auto vactions = childGet(call.params, 5);
2127+ GVariantIter iactions = {0};
2128+ g_variant_iter_init(&iactions, vactions.get());
2129+ const gchar * action = nullptr;
2130+ while (g_variant_iter_loop(&iactions, "&s", &action)) {
2131+ std::string saction(action);
2132+ notification.actions.push_back(saction);
2133+ }
2134+
2135+ auto vhints = childGet(call.params, 6);
2136+ GVariantIter ihints = {0};
2137+ g_variant_iter_init(&ihints, vhints.get());
2138+ const gchar * hint_key = nullptr;
2139+ GVariant * hint_value = nullptr;
2140+ while (g_variant_iter_loop(&ihints, "{&sv}", &hint_key, &hint_value)) {
2141+ std::string key(hint_key);
2142+ std::shared_ptr<GVariant> value(g_variant_ref(hint_value), [](GVariant * v){ if (v != nullptr) g_variant_unref(v); });
2143+ notification.hints[key] = value;
2144+ }
2145+
2146+ notifications.push_back(notification);
2147+ }
2148+
2149+ return notifications;
2150+ }
2151+
2152+ bool clearNotifications (void) {
2153+ return dbus_test_dbus_mock_object_clear_method_calls(mock, baseobj, nullptr);
2154+ }
2155+};
2156
2157=== added file 'tests/notifications-test.cc'
2158--- tests/notifications-test.cc 1970-01-01 00:00:00 +0000
2159+++ tests/notifications-test.cc 2015-02-14 02:38:32 +0000
2160@@ -0,0 +1,348 @@
2161+/*
2162+ * Copyright © 2015 Canonical Ltd.
2163+ *
2164+ * This program is free software; you can redistribute it and/or modify
2165+ * it under the terms of the GNU General Public License as published by
2166+ * the Free Software Foundation; version 3.
2167+ *
2168+ * This program is distributed in the hope that it will be useful,
2169+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2170+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2171+ * GNU General Public License for more details.
2172+ *
2173+ * You should have received a copy of the GNU General Public License
2174+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2175+ *
2176+ * Authors:
2177+ * Ted Gould <ted@canonical.com>
2178+ */
2179+
2180+#include <memory>
2181+
2182+#include <gtest/gtest.h>
2183+#include <gio/gio.h>
2184+#include <libdbustest/dbus-test.h>
2185+#include <libnotify/notify.h>
2186+
2187+#include "notifications-mock.h"
2188+#include "gtest-gvariant.h"
2189+
2190+extern "C" {
2191+#include "indicator-sound-service.h"
2192+#include "vala-mocks.h"
2193+}
2194+
2195+class NotificationsTest : public ::testing::Test
2196+{
2197+ protected:
2198+ DbusTestService * service = NULL;
2199+
2200+ GDBusConnection * session = NULL;
2201+ std::shared_ptr<NotificationsMock> notifications;
2202+
2203+ virtual void SetUp() {
2204+ g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, TRUE);
2205+ g_setenv("GSETTINGS_BACKEND", "memory", TRUE);
2206+
2207+ service = dbus_test_service_new(NULL);
2208+ dbus_test_service_set_bus(service, DBUS_TEST_SERVICE_BUS_SESSION);
2209+
2210+ /* Useful for debugging test failures, not needed all the time (until it fails) */
2211+ #if 0
2212+ auto bustle = std::shared_ptr<DbusTestTask>([]() {
2213+ DbusTestTask * bustle = DBUS_TEST_TASK(dbus_test_bustle_new("notifications-test.bustle"));
2214+ dbus_test_task_set_name(bustle, "Bustle");
2215+ dbus_test_task_set_bus(bustle, DBUS_TEST_SERVICE_BUS_SESSION);
2216+ return bustle;
2217+ }(), [](DbusTestTask * bustle) {
2218+ g_clear_object(&bustle);
2219+ });
2220+ dbus_test_service_add_task(service, bustle.get());
2221+ #endif
2222+
2223+ notifications = std::make_shared<NotificationsMock>();
2224+
2225+ dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
2226+ dbus_test_service_start_tasks(service);
2227+
2228+ session = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
2229+ ASSERT_NE(nullptr, session);
2230+ g_dbus_connection_set_exit_on_close(session, FALSE);
2231+ g_object_add_weak_pointer(G_OBJECT(session), (gpointer *)&session);
2232+
2233+ /* This is done in main.c */
2234+ notify_init("indicator-sound");
2235+ }
2236+
2237+ virtual void TearDown() {
2238+ if (notify_is_initted())
2239+ notify_uninit();
2240+
2241+ notifications.reset();
2242+ g_clear_object(&service);
2243+
2244+ g_object_unref(session);
2245+
2246+ unsigned int cleartry = 0;
2247+ while (session != NULL && cleartry < 100) {
2248+ loop(100);
2249+ cleartry++;
2250+ }
2251+
2252+ ASSERT_EQ(nullptr, session);
2253+ }
2254+
2255+ static gboolean timeout_cb (gpointer user_data) {
2256+ GMainLoop * loop = static_cast<GMainLoop *>(user_data);
2257+ g_main_loop_quit(loop);
2258+ return G_SOURCE_REMOVE;
2259+ }
2260+
2261+ void loop (unsigned int ms) {
2262+ GMainLoop * loop = g_main_loop_new(NULL, FALSE);
2263+ g_timeout_add(ms, timeout_cb, loop);
2264+ g_main_loop_run(loop);
2265+ g_main_loop_unref(loop);
2266+ }
2267+
2268+ static int unref_idle (gpointer user_data) {
2269+ g_variant_unref(static_cast<GVariant *>(user_data));
2270+ return G_SOURCE_REMOVE;
2271+ }
2272+
2273+ std::shared_ptr<MediaPlayerList> playerListMock () {
2274+ auto playerList = std::shared_ptr<MediaPlayerList>(
2275+ MEDIA_PLAYER_LIST(media_player_list_mock_new()),
2276+ [](MediaPlayerList * list) {
2277+ g_clear_object(&list);
2278+ });
2279+ return playerList;
2280+ }
2281+
2282+ std::shared_ptr<VolumeControl> volumeControlMock () {
2283+ auto volumeControl = std::shared_ptr<VolumeControl>(
2284+ VOLUME_CONTROL(volume_control_mock_new()),
2285+ [](VolumeControl * control){
2286+ g_clear_object(&control);
2287+ });
2288+ return volumeControl;
2289+ }
2290+
2291+ std::shared_ptr<IndicatorSoundService> standardService (std::shared_ptr<VolumeControl> volumeControl, std::shared_ptr<MediaPlayerList> playerList) {
2292+ auto soundService = std::shared_ptr<IndicatorSoundService>(
2293+ indicator_sound_service_new(playerList.get(), volumeControl.get(), nullptr),
2294+ [](IndicatorSoundService * service){
2295+ g_clear_object(&service);
2296+ });
2297+
2298+ return soundService;
2299+ }
2300+};
2301+
2302+TEST_F(NotificationsTest, BasicObject) {
2303+ auto soundService = standardService(volumeControlMock(), playerListMock());
2304+
2305+ /* Give some time settle */
2306+ loop(50);
2307+
2308+ /* Auto free */
2309+}
2310+
2311+TEST_F(NotificationsTest, VolumeChanges) {
2312+ auto volumeControl = volumeControlMock();
2313+ auto soundService = standardService(volumeControl, playerListMock());
2314+
2315+ /* Set a volume */
2316+ notifications->clearNotifications();
2317+ volume_control_set_volume(volumeControl.get(), 0.50);
2318+ loop(50);
2319+ auto notev = notifications->getNotifications();
2320+ ASSERT_EQ(1, notev.size());
2321+ EXPECT_EQ("indicator-sound", notev[0].app_name);
2322+ EXPECT_EQ("Volume", notev[0].summary);
2323+ EXPECT_EQ(0, notev[0].actions.size());
2324+ EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-private-synchronous"]);
2325+ EXPECT_GVARIANT_EQ("@i 50", notev[0].hints["value"]);
2326+
2327+ /* Set a different volume */
2328+ notifications->clearNotifications();
2329+ volume_control_set_volume(volumeControl.get(), 0.60);
2330+ loop(50);
2331+ notev = notifications->getNotifications();
2332+ ASSERT_EQ(1, notev.size());
2333+ EXPECT_GVARIANT_EQ("@i 60", notev[0].hints["value"]);
2334+
2335+ /* Set the same volume */
2336+ notifications->clearNotifications();
2337+ volume_control_set_volume(volumeControl.get(), 0.60);
2338+ loop(50);
2339+ notev = notifications->getNotifications();
2340+ ASSERT_EQ(0, notev.size());
2341+
2342+ /* Change just a little */
2343+ notifications->clearNotifications();
2344+ volume_control_set_volume(volumeControl.get(), 0.60001);
2345+ loop(50);
2346+ notev = notifications->getNotifications();
2347+ ASSERT_EQ(0, notev.size());
2348+}
2349+
2350+TEST_F(NotificationsTest, StreamChanges) {
2351+ auto volumeControl = volumeControlMock();
2352+ auto soundService = standardService(volumeControl, playerListMock());
2353+
2354+ /* Set a volume */
2355+ notifications->clearNotifications();
2356+ volume_control_set_volume(volumeControl.get(), 0.5);
2357+ loop(50);
2358+ auto notev = notifications->getNotifications();
2359+ ASSERT_EQ(1, notev.size());
2360+
2361+ /* Change Streams, no volume change */
2362+ notifications->clearNotifications();
2363+ volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "alarm");
2364+ volume_control_set_volume(volumeControl.get(), 0.5);
2365+ loop(50);
2366+ notev = notifications->getNotifications();
2367+ EXPECT_EQ(0, notev.size());
2368+
2369+ /* Change Streams, volume change */
2370+ notifications->clearNotifications();
2371+ volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "alert");
2372+ volume_control_set_volume(volumeControl.get(), 0.60);
2373+ loop(50);
2374+ notev = notifications->getNotifications();
2375+ EXPECT_EQ(0, notev.size());
2376+
2377+ /* Change Streams, no volume change, volume up */
2378+ notifications->clearNotifications();
2379+ volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "multimedia");
2380+ volume_control_set_volume(volumeControl.get(), 0.60);
2381+ loop(50);
2382+ volume_control_set_volume(volumeControl.get(), 0.65);
2383+ notev = notifications->getNotifications();
2384+ EXPECT_EQ(1, notev.size());
2385+ EXPECT_GVARIANT_EQ("@i 65", notev[0].hints["value"]);
2386+}
2387+
2388+TEST_F(NotificationsTest, IconTesting) {
2389+ auto volumeControl = volumeControlMock();
2390+ auto soundService = standardService(volumeControl, playerListMock());
2391+
2392+ /* Set an initial volume */
2393+ notifications->clearNotifications();
2394+ volume_control_set_volume(volumeControl.get(), 0.5);
2395+ loop(50);
2396+ auto notev = notifications->getNotifications();
2397+ ASSERT_EQ(1, notev.size());
2398+
2399+ /* Generate a set of notifications */
2400+ notifications->clearNotifications();
2401+ for (float i = 0.0; i < 1.01; i += 0.1) {
2402+ volume_control_set_volume(volumeControl.get(), i);
2403+ }
2404+
2405+ loop(50);
2406+ notev = notifications->getNotifications();
2407+ ASSERT_EQ(11, notev.size());
2408+
2409+ EXPECT_EQ("audio-volume-muted", notev[0].app_icon);
2410+ EXPECT_EQ("audio-volume-low", notev[1].app_icon);
2411+ EXPECT_EQ("audio-volume-low", notev[2].app_icon);
2412+ EXPECT_EQ("audio-volume-medium", notev[3].app_icon);
2413+ EXPECT_EQ("audio-volume-medium", notev[4].app_icon);
2414+ EXPECT_EQ("audio-volume-medium", notev[5].app_icon);
2415+ EXPECT_EQ("audio-volume-medium", notev[6].app_icon);
2416+ EXPECT_EQ("audio-volume-high", notev[7].app_icon);
2417+ EXPECT_EQ("audio-volume-high", notev[8].app_icon);
2418+ EXPECT_EQ("audio-volume-high", notev[9].app_icon);
2419+ EXPECT_EQ("audio-volume-high", notev[10].app_icon);
2420+}
2421+
2422+TEST_F(NotificationsTest, ServerRestart) {
2423+ auto volumeControl = volumeControlMock();
2424+ auto soundService = standardService(volumeControl, playerListMock());
2425+
2426+ /* Set a volume */
2427+ notifications->clearNotifications();
2428+ volume_control_set_volume(volumeControl.get(), 0.50);
2429+ loop(50);
2430+ auto notev = notifications->getNotifications();
2431+ ASSERT_EQ(1, notev.size());
2432+
2433+ /* Restart server without sync notifications */
2434+ notifications->clearNotifications();
2435+ dbus_test_service_remove_task(service, (DbusTestTask*)*notifications);
2436+ notifications.reset();
2437+
2438+ loop(50);
2439+
2440+ notifications = std::make_shared<NotificationsMock>(std::vector<std::string>({"body", "body-markup", "icon-static"}));
2441+ dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
2442+ dbus_test_task_run((DbusTestTask*)*notifications);
2443+
2444+ /* Change the volume */
2445+ notifications->clearNotifications();
2446+ volume_control_set_volume(volumeControl.get(), 0.60);
2447+ loop(50);
2448+ notev = notifications->getNotifications();
2449+ ASSERT_EQ(0, notev.size());
2450+
2451+ /* Put a good server back */
2452+ dbus_test_service_remove_task(service, (DbusTestTask*)*notifications);
2453+ notifications.reset();
2454+
2455+ loop(50);
2456+
2457+ notifications = std::make_shared<NotificationsMock>();
2458+ dbus_test_service_add_task(service, (DbusTestTask*)*notifications);
2459+ dbus_test_task_run((DbusTestTask*)*notifications);
2460+
2461+ /* Change the volume again */
2462+ notifications->clearNotifications();
2463+ volume_control_set_volume(volumeControl.get(), 0.70);
2464+ loop(50);
2465+ notev = notifications->getNotifications();
2466+ ASSERT_EQ(1, notev.size());
2467+}
2468+
2469+TEST_F(NotificationsTest, HighVolume) {
2470+ auto volumeControl = volumeControlMock();
2471+ auto soundService = standardService(volumeControl, playerListMock());
2472+
2473+ /* Set a volume */
2474+ notifications->clearNotifications();
2475+ volume_control_set_volume(volumeControl.get(), 0.50);
2476+ loop(50);
2477+ auto notev = notifications->getNotifications();
2478+ ASSERT_EQ(1, notev.size());
2479+ EXPECT_EQ("Volume", notev[0].summary);
2480+ EXPECT_GVARIANT_EQ("@s 'false'", notev[0].hints["x-canonical-value-bar-tint"]);
2481+
2482+ /* Set high volume with volume change */
2483+ notifications->clearNotifications();
2484+ volume_control_mock_set_mock_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), TRUE);
2485+ volume_control_set_volume(volumeControl.get(), 0.90);
2486+ loop(50);
2487+ notev = notifications->getNotifications();
2488+ 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 */
2489+ EXPECT_EQ("Volume", notev[0].summary);
2490+ EXPECT_EQ("High volume", notev[0].body);
2491+ EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-value-bar-tint"]);
2492+
2493+ /* Move it back */
2494+ volume_control_mock_set_mock_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), FALSE);
2495+ volume_control_set_volume(volumeControl.get(), 0.50);
2496+ loop(50);
2497+
2498+ /* Set high volume without level change */
2499+ /* NOTE: This can happen if headphones are plugged in */
2500+ notifications->clearNotifications();
2501+ volume_control_mock_set_mock_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), TRUE);
2502+ loop(50);
2503+ notev = notifications->getNotifications();
2504+ ASSERT_EQ(1, notev.size());
2505+ EXPECT_EQ("Volume", notev[0].summary);
2506+ EXPECT_EQ("High volume", notev[0].body);
2507+ EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-value-bar-tint"]);
2508+}
2509
2510=== modified file 'tests/pa-mock.cpp'
2511--- tests/pa-mock.cpp 2015-01-29 14:34:50 +0000
2512+++ tests/pa-mock.cpp 2015-02-14 02:38:32 +0000
2513@@ -23,6 +23,7 @@
2514
2515 #include <pulse/pulseaudio.h>
2516 #include <pulse/glib-mainloop.h>
2517+#include <pulse/ext-stream-restore.h>
2518 #include <gio/gio.h>
2519 #include <math.h>
2520
2521@@ -280,6 +281,12 @@
2522 return dummy_operation();
2523 }
2524
2525+pa_operation *
2526+pa_context_get_sink_input_info_list (pa_context *c, pa_sink_input_info_cb_t cb, void * userdata)
2527+{
2528+ return pa_context_get_sink_input_info (c, 0, cb, userdata);
2529+}
2530+
2531 pa_operation*
2532 pa_context_get_source_info_by_name (pa_context *c, const char * name, pa_source_info_cb_t cb, void *userdata)
2533 {
2534@@ -569,3 +576,18 @@
2535 return cvol;
2536 }
2537
2538+/* *******************************
2539+ * ext-stream-restore.h
2540+ * *******************************/
2541+
2542+pa_operation *
2543+pa_ext_stream_restore_test (pa_context * c, pa_ext_stream_restore_test_cb_t callback, void * userdata)
2544+{
2545+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
2546+ [c, callback, userdata]() {
2547+ if (callback != nullptr)
2548+ callback(c, 1, userdata);
2549+ });
2550+
2551+ return dummy_operation();
2552+}
2553
2554=== added file 'tests/volume-control-mock.vala'
2555--- tests/volume-control-mock.vala 1970-01-01 00:00:00 +0000
2556+++ tests/volume-control-mock.vala 2015-02-14 02:38:32 +0000
2557@@ -0,0 +1,43 @@
2558+/*
2559+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
2560+ * Copyright © 2015 Canonical Ltd.
2561+ *
2562+ * This program is free software; you can redistribute it and/or modify
2563+ * it under the terms of the GNU General Public License as published by
2564+ * the Free Software Foundation; version 3.
2565+ *
2566+ * This program is distributed in the hope that it will be useful,
2567+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2568+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2569+ * GNU General Public License for more details.
2570+ *
2571+ * You should have received a copy of the GNU General Public License
2572+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2573+ *
2574+ * Authors:
2575+ * Ted Gould <ted@canonical.com>
2576+ */
2577+
2578+public class VolumeControlMock : VolumeControl
2579+{
2580+ public string mock_stream { get; set; default = "multimedia"; }
2581+ public override string stream { get { return mock_stream; } }
2582+ public override bool ready { get; set; }
2583+ public override bool active_mic { get; set; }
2584+ public bool mock_high_volume { get; set; }
2585+ public override bool high_volume { get { return mock_high_volume; } }
2586+ public bool mock_mute { get; set; }
2587+ public override bool mute { get { return mock_mute; } set { } }
2588+ public bool mock_is_playing { get; set; }
2589+ public override bool is_playing { get { return mock_is_playing; } set { } }
2590+ public override double volume { get; set; }
2591+ public override double mic_volume { get; set; }
2592+
2593+ public VolumeControlMock() {
2594+ ready = true;
2595+ this.notify["mock-stream"].connect(() => this.notify_property("stream"));
2596+ this.notify["mock-high-volume"].connect(() => this.notify_property("high-volume"));
2597+ this.notify["mock-mute"].connect(() => this.notify_property("mute"));
2598+ this.notify["mock-is-playing"].connect(() => this.notify_property("is-playing"));
2599+ }
2600+}
2601
2602=== modified file 'tests/volume-control-test.cc'
2603--- tests/volume-control-test.cc 2015-01-27 17:42:26 +0000
2604+++ tests/volume-control-test.cc 2015-02-14 02:38:32 +0000
2605@@ -23,6 +23,7 @@
2606
2607 extern "C" {
2608 #include "indicator-sound-service.h"
2609+#include "vala-mocks.h"
2610 }
2611
2612 class VolumeControlTest : public ::testing::Test
2613@@ -71,13 +72,14 @@
2614 };
2615
2616 TEST_F(VolumeControlTest, BasicObject) {
2617- VolumeControl * control = volume_control_new();
2618+ FocusTrackerMock * focustracker = focus_tracker_mock_new();
2619+ VolumeControlPulse * control = volume_control_pulse_new(FOCUS_TRACKER(focustracker));
2620
2621 /* Setup the PA backend */
2622 loop(100);
2623
2624 /* Ready */
2625- EXPECT_TRUE(volume_control_get_ready(control));
2626+ EXPECT_TRUE(volume_control_get_ready(VOLUME_CONTROL(control)));
2627
2628 g_clear_object(&control);
2629 }
2630
2631=== added file 'vapi/libpulse-ext-stream-restore.vapi'
2632--- vapi/libpulse-ext-stream-restore.vapi 1970-01-01 00:00:00 +0000
2633+++ vapi/libpulse-ext-stream-restore.vapi 2015-02-14 02:38:32 +0000
2634@@ -0,0 +1,22 @@
2635+
2636+using GLib;
2637+using PulseAudio;
2638+
2639+[CCode (cheader_filename="pulse/ext-stream-restore.h")]
2640+namespace PulseAudio.Extension.StreamRestore {
2641+ public struct Info {
2642+ string name;
2643+ PulseAudio.ChannelMap channel_map;
2644+ PulseAudio.CVolume volume;
2645+ string device;
2646+ int mute;
2647+ }
2648+
2649+ public delegate void TestCb (PulseAudio.Context context, uint32 version);
2650+ [CCode (cname="pa_ext_stream_restore_test")]
2651+ PulseAudio.Operation? test (PulseAudio.Context context, TestCb? callback = null);
2652+
2653+ public delegate void ReadCb (PulseAudio.Context context, PulseAudio.Extension.StreamRestore.Info[] streams, int eol);
2654+ [CCode (cname="pa_ext_stream_restore_read")]
2655+ PulseAudio.Operation? read (PulseAudio.Context context, ReadCb? callback = null);
2656+}

Subscribers

People subscribed via source and target branches