Merge lp:~ted/indicator-sound/sound-stream-cleanup into lp:indicator-sound/15.04
- sound-stream-cleanup
- Merge into trunk.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 |
Related bugs: |
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.
Commit message
Description of the change
To post a comment you must log in.
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 | +} |