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

Proposed by Ted Gould
Status: Superseded
Proposed branch: lp:~ted/indicator-sound/sound-stream-cleanup
Merge into: lp:indicator-sound/15.04
Prerequisite: lp:~ted/indicator-sound/notification-cleanup
Diff against target: 2215 lines (+1174/-512)
19 files modified
CMakeLists.txt (+1/-1)
debian/control (+2/-2)
src/CMakeLists.txt (+13/-0)
src/focus-tracker-stack.vala (+88/-0)
src/focus-tracker.vala (+22/-0)
src/main.c (+22/-4)
src/media-player-list.vala (+1/-1)
src/service.vala (+23/-4)
src/volume-control.vala (+315/-486)
tests/CMakeLists.txt (+32/-0)
tests/accounts-service-mock.h (+21/-0)
tests/accounts-service-user.cc (+1/-2)
tests/focus-tracker-mock.vala (+27/-0)
tests/indicator-fixture.h (+444/-0)
tests/indicator-test.cc (+110/-0)
tests/media-player-user.cc (+5/-11)
tests/pa-mock.cpp (+22/-0)
tests/volume-control-test.cc (+3/-1)
vapi/libpulse-ext-stream-restore.vapi (+22/-0)
To merge this branch: bzr merge lp:~ted/indicator-sound/sound-stream-cleanup
Reviewer Review Type Date Requested Status
Indicator Applet Developers Pending
Review via email: mp+245053@code.launchpad.net

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

This proposal has been superseded by a proposal from 2015-01-30.

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

Merging the indicator-test branch

499. By Ted Gould

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

500. By Ted Gould

Add a focus tracker mock

501. By Ted Gould

Add a mock symbol for stream restore checking

502. By Ted Gould

Adding another missing symbol

503. By Ted Gould

Merging in the notifications mock branch

Unmerged revisions

503. By Ted Gould

Merging in the notifications mock branch

502. By Ted Gould

Adding another missing symbol

501. By Ted Gould

Add a mock symbol for stream restore checking

500. By Ted Gould

Add a focus tracker mock

499. By Ted Gould

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

498. By Ted Gould

Merging the indicator-test branch

497. By Ted Gould

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

496. By Ted Gould

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

495. By Ted Gould

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

494. By Ted Gould

Bind the property into a value in volume control

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2014-02-12 02:00:36 +0000
3+++ CMakeLists.txt 2015-01-30 23:01:09 +0000
4@@ -49,7 +49,7 @@
5
6 pkg_check_modules(
7 TEST REQUIRED
8- dbustest-1
9+ dbustest-1>=15.04.0
10 )
11 include_directories(${TEST_INCLUDE_DIRS})
12
13
14=== modified file 'debian/control'
15--- debian/control 2014-10-03 20:52:10 +0000
16+++ debian/control 2015-01-30 23:01:09 +0000
17@@ -6,14 +6,14 @@
18 Build-Depends: debhelper (>= 9.0),
19 cmake,
20 dbus,
21- dbus-test-runner (>= 14.04.0+14.04.20140226),
22+ dbus-test-runner (>> 14.04.0+14.04.20150120.1),
23 dh-translations,
24 gir1.2-accountsservice-1.0,
25 gnome-common,
26 autotools-dev,
27 valac (>= 0.20),
28 libaccountsservice-dev,
29- libdbustest1-dev (>= 14.04.1),
30+ libdbustest1-dev (>= 15.04.0),
31 libgirepository1.0-dev,
32 libglib2.0-dev (>= 2.22.3),
33 libgtest-dev,
34
35=== modified file 'src/CMakeLists.txt'
36--- src/CMakeLists.txt 2015-01-27 17:42:26 +0000
37+++ src/CMakeLists.txt 2015-01-30 23:01:09 +0000
38@@ -36,6 +36,7 @@
39 --vapidir=.
40 --pkg=url-dispatcher
41 --pkg=bus-watcher
42+ --pkg=libpulse-ext-stream-restore
43 )
44
45 vala_add(indicator-sound-service
46@@ -47,9 +48,12 @@
47 media-player-list
48 mpris2-interfaces
49 accounts-service-user
50+ focus-tracker
51 )
52 vala_add(indicator-sound-service
53 volume-control.vala
54+ DEPENDS
55+ focus-tracker
56 )
57 vala_add(indicator-sound-service
58 media-player.vala
59@@ -120,6 +124,14 @@
60 vala_add(indicator-sound-service
61 greeter-broadcast.vala
62 )
63+vala_add(indicator-sound-service
64+ focus-tracker.vala
65+)
66+vala_add(indicator-sound-service
67+ focus-tracker-stack.vala
68+ DEPENDS
69+ focus-tracker
70+)
71
72 vala_finish(indicator-sound-service
73 SOURCES
74@@ -154,6 +166,7 @@
75
76 add_definitions(
77 -w
78+ -DG_LOG_DOMAIN="indicator-sound"
79 )
80
81 add_library(
82
83=== added file 'src/focus-tracker-stack.vala'
84--- src/focus-tracker-stack.vala 1970-01-01 00:00:00 +0000
85+++ src/focus-tracker-stack.vala 2015-01-30 23:01:09 +0000
86@@ -0,0 +1,88 @@
87+/*
88+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
89+ * Copyright © 2014 Canonical Ltd.
90+ *
91+ * This program is free software; you can redistribute it and/or modify
92+ * it under the terms of the GNU General Public License as published by
93+ * the Free Software Foundation; version 3.
94+ *
95+ * This program is distributed in the hope that it will be useful,
96+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
97+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
98+ * GNU General Public License for more details.
99+ *
100+ * You should have received a copy of the GNU General Public License
101+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
102+ *
103+ * Authors:
104+ * Ted Gould <ted@canonical.com>
105+ */
106+
107+private struct WindowInfo {
108+ public uint window_id;
109+ public string app_id;
110+ public bool focused;
111+ public uint stage;
112+}
113+
114+/*
115+private struct DesktopInfo {
116+ public string app_id;
117+ public string desktop_file;
118+}
119+*/
120+
121+[DBus (name = "com.canonical.Unity.WindowStack")]
122+private interface WindowStack : Object {
123+ /* public abstract async DesktopInfo GetAppIdFromPid (uint pid) throws IOError; */
124+ public abstract async WindowInfo[] GetWindowStack () throws IOError;
125+ /* public abstract async string[] GetWindowProperties (uint window_id, string app_id, string[] names) throws IOError; */
126+
127+ public signal void FocusedWindowChanged (uint window_id, string app_id, uint stage);
128+ /* public signal void WindowCreated (uint window_id, string app_id); */
129+ /* public signal void WindowDestroyed (uint window_id, string app_id); */
130+}
131+
132+public class FocusTrackerStack : FocusTracker {
133+ public override string focused_appid { get; private set; default = "unknown"; }
134+ private WindowStack proxy;
135+
136+ public FocusTrackerStack ( ) {
137+ build_proxies.begin();
138+ }
139+
140+ private async void build_proxies() {
141+ try {
142+ proxy = yield Bus.get_proxy<WindowStack>(BusType.SESSION,
143+ "com.canonical.Unity.WindowStack",
144+ "/com/canonical/Unity/WindowStack",
145+ DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
146+ null);
147+
148+ proxy.FocusedWindowChanged.connect((window, appid, stage) => {
149+ if (stage == 0) {
150+ debug("Focus changed to: %s", appid);
151+ this.focused_appid = appid;
152+ }
153+ });
154+
155+ update_focused_appid.begin();
156+ } catch (Error e) {
157+ warning("Unable to create window stack proxy: %s", e.message);
158+ }
159+ }
160+
161+ private async void update_focused_appid () {
162+ try {
163+ var windows = yield proxy.GetWindowStack();
164+ foreach (var window in windows) {
165+ if (window.focused && window.stage == 0) {
166+ focused_appid = window.app_id;
167+ break;
168+ }
169+ }
170+ } catch (Error e) {
171+ warning("Unable to get window stack list: %s", e.message);
172+ }
173+ }
174+}
175
176=== added file 'src/focus-tracker.vala'
177--- src/focus-tracker.vala 1970-01-01 00:00:00 +0000
178+++ src/focus-tracker.vala 2015-01-30 23:01:09 +0000
179@@ -0,0 +1,22 @@
180+/*
181+ * Copyright © 2015 Canonical Ltd.
182+ *
183+ * This program is free software; you can redistribute it and/or modify
184+ * it under the terms of the GNU General Public License as published by
185+ * the Free Software Foundation; version 3.
186+ *
187+ * This program is distributed in the hope that it will be useful,
188+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
189+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
190+ * GNU General Public License for more details.
191+ *
192+ * You should have received a copy of the GNU General Public License
193+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
194+ *
195+ * Authors:
196+ * Ted Gould <ted@canonical.com>
197+ */
198+
199+public abstract class FocusTracker : Object {
200+ public virtual string focused_appid { get; protected set; }
201+}
202
203=== modified file 'src/main.c'
204--- src/main.c 2014-02-25 22:47:45 +0000
205+++ src/main.c 2015-01-30 23:01:09 +0000
206@@ -1,6 +1,21 @@
207-/* main.c generated by valac 0.22.1, the Vala compiler
208- * generated from main.vala, do not modify */
209-
210+/*
211+ * Copyright © 2015 Canonical Ltd.
212+ *
213+ * This program is free software; you can redistribute it and/or modify
214+ * it under the terms of the GNU General Public License as published by
215+ * the Free Software Foundation; version 3.
216+ *
217+ * This program is distributed in the hope that it will be useful,
218+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
219+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
220+ * GNU General Public License for more details.
221+ *
222+ * You should have received a copy of the GNU General Public License
223+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
224+ *
225+ * Authors:
226+ * Ted Gould <ted@canonical.com>
227+ */
228
229 #include <glib.h>
230 #include <locale.h>
231@@ -29,10 +44,13 @@
232 playerlist = MEDIA_PLAYER_LIST(media_player_list_mpris_new());
233 }
234
235- service = indicator_sound_service_new (playerlist);
236+ FocusTracker * tracker = FOCUS_TRACKER(focus_tracker_stack_new());
237+
238+ service = indicator_sound_service_new (playerlist, tracker);
239 result = indicator_sound_service_run (service);
240
241 g_object_unref(playerlist);
242+ g_object_unref(tracker);
243 g_object_unref(service);
244
245 return result;
246
247=== modified file 'src/media-player-list.vala'
248--- src/media-player-list.vala 2014-02-24 22:47:50 +0000
249+++ src/media-player-list.vala 2015-01-30 23:01:09 +0000
250@@ -17,7 +17,7 @@
251 * Ted Gould <ted@canonical.com>
252 */
253
254-public class MediaPlayerList {
255+public class MediaPlayerList : Object {
256 public class Iterator {
257 public virtual MediaPlayer? next_value() {
258 return null;
259
260=== modified file 'src/service.vala'
261--- src/service.vala 2015-01-29 17:31:03 +0000
262+++ src/service.vala 2015-01-30 23:01:09 +0000
263@@ -18,7 +18,7 @@
264 */
265
266 public class IndicatorSound.Service: Object {
267- public Service (MediaPlayerList playerlist) {
268+ public Service (MediaPlayerList playerlist, FocusTracker tracker) {
269 sync_notification = new Notify.Notification(_("Volume"), "", "audio-volume-muted");
270 this.notification_server_watch = GLib.Bus.watch_name(GLib.BusType.SESSION,
271 "org.freedesktop.Notifications",
272@@ -32,7 +32,13 @@
273 this.settings.bind ("visible", this, "visible", SettingsBindFlags.GET);
274 this.notify["visible"].connect ( () => this.update_root_icon () );
275
276- this.volume_control = new VolumeControl ();
277+ this.focus_tracker = tracker;
278+
279+ this.volume_control = new VolumeControl (this.focus_tracker);
280+ this.volume_control.notify["stream"].connect(() => {
281+ this.update_sync_notification();
282+ });
283+ this.volume_control.bind_property ("media-playing", this, "player-playing", BindingFlags.SYNC_CREATE);
284
285 /* If we're on the greeter, don't export */
286 if (GLib.Environment.get_user_name() != "lightdm") {
287@@ -177,7 +183,9 @@
288 Settings settings;
289 Settings sharedsettings;
290 VolumeControl volume_control;
291+ FocusTracker focus_tracker;
292 MediaPlayerList players;
293+ public bool player_playing { get; set; default = false; }
294 uint player_action_update_id;
295 bool mute_blocks_sound;
296 uint sound_was_blocked_timeout_id;
297@@ -288,7 +296,7 @@
298 return;
299
300 /* Determine Label */
301- string volume_label = "";
302+ string volume_label = volume_control.stream; /* TODO: Undo this */
303 if (volume_control.high_volume)
304 volume_label = _("High volume");
305
306@@ -362,7 +370,7 @@
307 });
308
309 mute_action.change_state.connect ( (action, val) => {
310- volume_control.set_mute (val.get_boolean ());
311+ volume_control.mute = val.get_boolean ();
312 });
313
314 this.volume_control.notify["mute"].connect ( () => {
315@@ -475,6 +483,7 @@
316 }
317
318 void name_lost (DBusConnection connection, string name) {
319+ warning("Unable to get DBus Name: %s", name);
320 this.loop.quit ();
321 }
322
323@@ -493,6 +502,7 @@
324
325 bool update_player_actions () {
326 bool clear_accounts_player = true;
327+ bool player_playing = false;
328
329 foreach (var player in this.players) {
330 SimpleAction? action = this.actions.lookup_action (player.id) as SimpleAction;
331@@ -512,11 +522,20 @@
332 accounts_service.player = player;
333 clear_accounts_player = false;
334 }
335+
336+ if (player.state == "Playing") {
337+ player_playing = true;
338+ }
339 }
340
341 if (clear_accounts_player)
342 clear_acts_player();
343
344+ if (this.player_playing != player_playing) {
345+ this.player_playing = player_playing;
346+ update_root_icon();
347+ }
348+
349 this.player_action_update_id = 0;
350 return false;
351 }
352
353=== modified file 'src/volume-control.vala'
354--- src/volume-control.vala 2015-01-29 17:31:55 +0000
355+++ src/volume-control.vala 2015-01-30 23:01:09 +0000
356@@ -19,6 +19,7 @@
357 */
358
359 using PulseAudio;
360+using PulseAudio.Extension.StreamRestore;
361 using Notify;
362 using Gee;
363
364@@ -37,29 +38,47 @@
365 /* this is static to ensure it being freed after @context (loop does not have ref counting) */
366 private static PulseAudio.GLibMainLoop loop;
367
368+ private FocusTracker focus_tracker;
369+ public bool media_playing { get; set; default = false; }
370+
371 private uint _reconnect_timer = 0;
372
373 private PulseAudio.Context context;
374 private bool _mute = true;
375- private bool _is_playing = false;
376+ public bool is_playing { get; private set; default = false; }
377 private double _volume = 0.0;
378 private double _mic_volume = 0.0;
379
380 /* Used by the pulseaudio stream restore extension */
381- private DBusConnection _pconn;
382- /* Need both the list and hash so we can retrieve the last known sink-input after
383- * releasing the current active one (restoring back to the previous known role) */
384- private Gee.ArrayList<uint32> _sink_input_list = new Gee.ArrayList<uint32> ();
385- private HashMap<uint32, string> _sink_input_hash = new HashMap<uint32, string> ();
386 private bool _pulse_use_stream_restore = false;
387- private uint32 _active_sink_input = -1;
388- private string[] _valid_roles = {"multimedia", "alert", "alarm", "phone"};
389- private string? _objp_role_multimedia = null;
390- private string? _objp_role_alert = null;
391- private string? _objp_role_alarm = null;
392- private string? _objp_role_phone = null;
393- private uint _pa_volume_sig_count = 0;
394-
395+ private enum InputStreamType {
396+ ALERT = 0,
397+ ALARM = 1,
398+ MULTIMEDIA = 2,
399+ PHONE = 3
400+ }
401+ private InputStreamType currentstream = InputStreamType.ALERT;
402+ private InputStreamType tempstream = InputStreamType.ALERT;
403+ /** The name of the stream that we're currently playing */
404+ public string stream {
405+ get {
406+ switch (this.currentstream) {
407+ case InputStreamType.ALERT:
408+ return "alert";
409+ case InputStreamType.ALARM:
410+ return "alarm";
411+ case InputStreamType.MULTIMEDIA:
412+ return "multimedia";
413+ case InputStreamType.PHONE:
414+ return "phone";
415+ }
416+
417+ return "alert";
418+ }
419+ }
420+
421+
422+ /* Accounts Service Variables */
423 private DBusProxy _user_proxy;
424 private GreeterListInterface _greeter_proxy;
425 private Cancellable _mute_cancellable;
426@@ -68,7 +87,14 @@
427 private uint _accountservice_volume_timer = 0;
428 private bool _send_next_local_volume = false;
429 private double _account_service_volume = 0.0;
430+
431+
432 private bool _active_port_headphone = false;
433+ /* There is not easy way to check if the port is a headset/headphone besides
434+ * checking for the port name. On touch (with the pulseaudio droid element)
435+ * the headset/headphone port is called 'output-headset' and 'output-headphone'.
436+ * On the desktop this is usually called 'analog-output-headphones' */
437+ private string[] headphone_outputs = {"output-wired_headset", "output-wired_headphone", "analog-output-headphones"};
438
439 /** true when connected to the pulse server */
440 public bool ready { get; set; }
441@@ -79,12 +105,20 @@
442 /** true when high volume warnings should be shown */
443 public bool high_volume {
444 get {
445- return this._volume > 0.75 && _active_port_headphone;
446+ return this._volume > 0.75 && _active_port_headphone && stream == "multimedia";
447 }
448 }
449
450- public VolumeControl ()
451+ public VolumeControl (FocusTracker tracker)
452 {
453+ this.focus_tracker = tracker;
454+ this.focus_tracker.notify["focused-appid"].connect(() => {
455+ if (!this.ready)
456+ return;
457+
458+ this.update_sink_input();
459+ });
460+
461 if (loop == null)
462 loop = new PulseAudio.GLibMainLoop ();
463
464@@ -94,6 +128,13 @@
465 setup_accountsservice.begin ();
466
467 this.reconnect_to_pulse ();
468+
469+ this.notify["media-playing"].connect(() => {
470+ update_sink_input();
471+ });
472+ this.notify["stream"].connect(() => {
473+ update_stream();
474+ });
475 }
476
477 ~VolumeControl ()
478@@ -106,7 +147,10 @@
479 stop_account_service_volume_timer();
480 }
481
482- /* PulseAudio logic*/
483+ /************************/
484+ /* PulseAudio logic */
485+ /************************/
486+
487 private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index)
488 {
489 switch (t & Context.SubscriptionEventType.FACILITY_MASK)
490@@ -116,23 +160,7 @@
491 break;
492
493 case Context.SubscriptionEventType.SINK_INPUT:
494- switch (t & Context.SubscriptionEventType.TYPE_MASK)
495- {
496- case Context.SubscriptionEventType.NEW:
497- c.get_sink_input_info (index, handle_new_sink_input_cb);
498- break;
499-
500- case Context.SubscriptionEventType.CHANGE:
501- c.get_sink_input_info (index, handle_changed_sink_input_cb);
502- break;
503-
504- case Context.SubscriptionEventType.REMOVE:
505- remove_sink_input_from_list (index);
506- break;
507- default:
508- debug ("Sink input event not known.");
509- break;
510- }
511+ update_sink_input ();
512 break;
513
514 case Context.SubscriptionEventType.SOURCE:
515@@ -143,7 +171,14 @@
516 switch (t & Context.SubscriptionEventType.TYPE_MASK)
517 {
518 case Context.SubscriptionEventType.NEW:
519- c.get_source_output_info (index, source_output_info_cb);
520+ c.get_source_output_info (index, (context, info, eol) => {
521+ if (info == null)
522+ return;
523+
524+ var role = info.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
525+ if (role == "phone" || role == "production")
526+ this.active_mic = true;
527+ });
528 break;
529
530 case Context.SubscriptionEventType.REMOVE:
531@@ -154,291 +189,205 @@
532 }
533 }
534
535- private void sink_info_cb_for_props (Context c, SinkInfo? i, int eol)
536- {
537- bool old_high_volume = this.high_volume;
538-
539- if (i == null)
540- return;
541-
542- if (_mute != (bool)i.mute)
543- {
544- _mute = (bool)i.mute;
545- this.notify_property ("mute");
546- }
547-
548- var playing = (i.state == PulseAudio.SinkState.RUNNING);
549- if (_is_playing != playing)
550- {
551- _is_playing = playing;
552- this.notify_property ("is-playing");
553- }
554-
555- /* Check if the current active port is headset/headphone */
556- /* There is not easy way to check if the port is a headset/headphone besides
557- * checking for the port name. On touch (with the pulseaudio droid element)
558- * the headset/headphone port is called 'output-headset' and 'output-headphone'.
559- * On the desktop this is usually called 'analog-output-headphones' */
560- if (i.active_port != null &&
561- (i.active_port.name == "output-wired_headset" ||
562- i.active_port.name == "output-wired_headphone" ||
563- i.active_port.name == "analog-output-headphones")) {
564- _active_port_headphone = true;
565- } else {
566- _active_port_headphone = false;
567- }
568-
569- if (_pulse_use_stream_restore == false &&
570- _volume != volume_to_double (i.volume.max ()))
571- {
572- _volume = volume_to_double (i.volume.max ());
573- this.notify_property("volume");
574- start_local_volume_timer();
575- }
576-
577- if (this.high_volume != old_high_volume) {
578- this.notify_property("high-volume");
579- }
580- }
581-
582- private void source_info_cb (Context c, SourceInfo? i, int eol)
583- {
584- if (i == null)
585- return;
586-
587- if (_mic_volume != volume_to_double (i.volume.values[0]))
588- {
589- _mic_volume = volume_to_double (i.volume.values[0]);
590- this.notify_property ("mic-volume");
591- }
592- }
593-
594- private void server_info_cb_for_props (Context c, ServerInfo? i)
595- {
596- if (i == null)
597- return;
598- context.get_sink_info_by_name (i.default_sink_name, sink_info_cb_for_props);
599- }
600-
601 private void update_sink ()
602 {
603- context.get_server_info (server_info_cb_for_props);
604- }
605-
606- private void update_source_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) {
607- if (i != null)
608- context.get_source_info_by_name (i.default_source_name, source_info_cb);
609+ if (!this.ready)
610+ return;
611+
612+ context.get_server_info ((context, info) => {
613+ if (info == null)
614+ return;
615+
616+ context.get_sink_info_by_name (info.default_sink_name, (context, info, eol) => {
617+ bool old_high_volume = this.high_volume;
618+
619+ if (info == null)
620+ return;
621+
622+ if (_mute != (bool)info.mute)
623+ {
624+ _mute = (bool)info.mute;
625+ this.notify_property ("mute");
626+ }
627+
628+ this.is_playing = (info.state == PulseAudio.SinkState.RUNNING);
629+
630+ /* Check if the current active port is headset/headphone */
631+ if (info.active_port.name in headphone_outputs) {
632+ _active_port_headphone = true;
633+ } else {
634+ _active_port_headphone = false;
635+ }
636+
637+ if (_pulse_use_stream_restore == false &&
638+ _volume != volume_to_double (info.volume.max ()))
639+ {
640+ _volume = volume_to_double (info.volume.max ());
641+ this.notify_property("volume");
642+ start_local_volume_timer();
643+ }
644+
645+ if (this.high_volume != old_high_volume) {
646+ this.notify_property("high-volume");
647+ }
648+ });
649+ });
650+ }
651+
652+ private void update_sink_input ()
653+ {
654+ if (!this.ready)
655+ return;
656+
657+ if (!this._pulse_use_stream_restore)
658+ return;
659+
660+ this.tempstream = InputStreamType.ALERT;
661+ if (this.media_playing)
662+ this.tempstream = InputStreamType.MULTIMEDIA;
663+
664+ context.get_sink_input_info_list((context, info, eol) => {
665+ var inputstream = InputStreamType.ALARM;
666+
667+ if (info != null) {
668+ var role = info.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
669+ var corked = (info.corked != 0);
670+ var appid = info.proplist.gets (PulseAudio.Proplist.PROP_APPLICATION_ID);
671+
672+ /* Override the corked value for the phone as it's always corked */
673+ if (role != null && role == "phone")
674+ corked = false;
675+
676+ if (!corked && role != null && appid == this.focus_tracker.focused_appid) {
677+ switch (role) {
678+ case "phone":
679+ inputstream = InputStreamType.PHONE;
680+ break;
681+ case "alarm":
682+ inputstream = InputStreamType.ALARM;
683+ break;
684+ case "alert":
685+ inputstream = InputStreamType.ALERT;
686+ break;
687+ case "multimedia":
688+ default:
689+ /* We treat unknown roles (i.e. 'music') as multimedia */
690+ inputstream = InputStreamType.MULTIMEDIA;
691+ break;
692+ }
693+ }
694+ }
695+
696+ if (inputstream > this.tempstream) {
697+ this.tempstream = inputstream;
698+ }
699+
700+ if (eol != 0) {
701+ if (this.currentstream != this.tempstream) {
702+ this.currentstream = this.tempstream;
703+ debug("Current stream: %s", this.stream);
704+ this.notify_property("stream");
705+ }
706+ }
707+ });
708+ }
709+
710+ /* Look at a given profile or role and figure out what our volume and
711+ mute settings should be based on that. */
712+ private void update_stream ()
713+ {
714+
715+
716 }
717
718 private void update_source ()
719 {
720- context.get_server_info (update_source_get_server_info_cb);
721- }
722-
723- private DBusMessage pulse_dbus_filter (DBusConnection connection, owned DBusMessage message, bool incoming)
724- {
725- if (message.get_message_type () == DBusMessageType.SIGNAL) {
726- string active_role_objp = _objp_role_alert;
727- if (_active_sink_input != -1)
728- active_role_objp = _sink_input_hash.get (_active_sink_input);
729-
730- if (message.get_path () == active_role_objp && message.get_member () == "VolumeUpdated") {
731- uint sig_count = 0;
732- lock (_pa_volume_sig_count) {
733- sig_count = _pa_volume_sig_count;
734- if (_pa_volume_sig_count > 0)
735- _pa_volume_sig_count--;
736- }
737-
738- /* We only care about signals if our internal count is zero */
739- if (sig_count == 0) {
740- /* Extract volume and make sure it's not a side effect of us setting it */
741- Variant body = message.get_body ();
742- Variant varray = body.get_child_value (0);
743-
744- uint32 type = 0, volume = 0;
745- VariantIter iter = varray.iterator ();
746- iter.next ("(uu)", &type, &volume);
747- /* Here we need to compare integer values to avoid rounding issues, so just
748- * using the volume values used by pulseaudio */
749- PulseAudio.Volume cvolume = double_to_volume (_volume);
750- if (volume != cvolume) {
751- /* Someone else changed the volume for this role, reflect on the indicator */
752- _volume = volume_to_double (volume);
753- this.notify_property("volume");
754- start_local_volume_timer();
755- }
756- }
757- }
758- }
759-
760- return message;
761- }
762-
763- private async void update_active_sink_input (uint32 index)
764- {
765- if ((index == -1) || (index != _active_sink_input && index in _sink_input_list)) {
766- string sink_input_objp = _objp_role_alert;
767- if (index != -1)
768- sink_input_objp = _sink_input_hash.get (index);
769- _active_sink_input = index;
770-
771- /* Listen for role volume changes from pulse itself (external clients) */
772- try {
773- var builder = new VariantBuilder (new VariantType ("ao"));
774- builder.add ("o", sink_input_objp);
775-
776- yield _pconn.call ("org.PulseAudio.Core1", "/org/pulseaudio/core1",
777- "org.PulseAudio.Core1", "ListenForSignal",
778- new Variant ("(sao)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry.VolumeUpdated", builder),
779- null, DBusCallFlags.NONE, -1);
780- } catch (GLib.Error e) {
781- warning ("unable to listen for pulseaudio dbus signals (%s)", e.message);
782- }
783-
784- try {
785- var props_variant = yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry",
786- sink_input_objp, "org.freedesktop.DBus.Properties", "Get",
787- new Variant ("(ss)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume"),
788- null, DBusCallFlags.NONE, -1);
789- Variant tmp;
790- props_variant.get ("(v)", out tmp);
791- uint32 type = 0, volume = 0;
792- VariantIter iter = tmp.iterator ();
793- iter.next ("(uu)", &type, &volume);
794-
795- _volume = volume_to_double (volume);
796- this.notify_property("volume");
797- start_local_volume_timer();
798- } catch (GLib.Error e) {
799- warning ("unable to get volume for active role %s (%s)", sink_input_objp, e.message);
800- }
801- }
802- }
803-
804- private void add_sink_input_into_list (SinkInputInfo sink_input)
805- {
806- /* We're only adding ones that are not corked and with a valid role */
807- var role = sink_input.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
808-
809- if (role != null && role in _valid_roles) {
810- if (sink_input.corked == 0 || role == "phone") {
811- _sink_input_list.insert (0, sink_input.index);
812- switch (role)
813+ if (!this.ready)
814+ return;
815+
816+ context.get_server_info ((context, info) => {
817+ if (info == null)
818+ return;
819+
820+ context.get_source_info_by_name (info.default_source_name, (context, info, eol) => {
821+ if (info == null)
822+ return;
823+
824+ if (_mic_volume != volume_to_double (info.volume.values[0]))
825 {
826- case "multimedia":
827- _sink_input_hash.set (sink_input.index, _objp_role_multimedia);
828- break;
829- case "alert":
830- _sink_input_hash.set (sink_input.index, _objp_role_alert);
831- break;
832- case "alarm":
833- _sink_input_hash.set (sink_input.index, _objp_role_alarm);
834- break;
835- case "phone":
836- _sink_input_hash.set (sink_input.index, _objp_role_phone);
837- break;
838+ _mic_volume = volume_to_double (info.volume.values[0]);
839+ this.notify_property ("mic-volume");
840 }
841- /* Only switch the active sink input in case a phone one is not active */
842- if (_active_sink_input == -1 ||
843- _sink_input_hash.get (_active_sink_input) != _objp_role_phone)
844- update_active_sink_input.begin (sink_input.index);
845- }
846- }
847- }
848-
849- private void remove_sink_input_from_list (uint32 index)
850- {
851- if (index in _sink_input_list) {
852- _sink_input_list.remove (index);
853- _sink_input_hash.unset (index);
854- if (index == _active_sink_input) {
855- if (_sink_input_list.size != 0)
856- update_active_sink_input.begin (_sink_input_list.get (0));
857- else
858- update_active_sink_input.begin (-1);
859- }
860- }
861- }
862-
863- private void handle_new_sink_input_cb (Context c, SinkInputInfo? i, int eol)
864- {
865- if (i == null)
866- return;
867-
868- add_sink_input_into_list (i);
869- }
870-
871- private void handle_changed_sink_input_cb (Context c, SinkInputInfo? i, int eol)
872- {
873- if (i == null)
874- return;
875-
876- if (i.index in _sink_input_list) {
877- /* Phone stream is always corked, so handle it differently */
878- if (i.corked == 1 && _sink_input_hash.get (i.index) != _objp_role_phone)
879- remove_sink_input_from_list (i.index);
880- } else {
881- if (i.corked == 0)
882- add_sink_input_into_list (i);
883- }
884- }
885-
886- private void source_output_info_cb (Context c, SourceOutputInfo? i, int eol)
887- {
888- if (i == null)
889- return;
890-
891- var role = i.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
892- if (role == "phone" || role == "production")
893- this.active_mic = true;
894+ });
895+ });
896 }
897
898 private void context_state_callback (Context c)
899 {
900 switch (c.get_state ()) {
901 case Context.State.READY:
902- if (_pulse_use_stream_restore) {
903- c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
904- PulseAudio.Context.SubscriptionMask.SINK_INPUT |
905- PulseAudio.Context.SubscriptionMask.SOURCE |
906- PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
907- } else {
908- c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
909- PulseAudio.Context.SubscriptionMask.SOURCE |
910- PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
911- }
912- c.set_subscribe_callback (context_events_cb);
913- update_sink ();
914- update_source ();
915- this.ready = true;
916+ PulseAudio.Extension.StreamRestore.test(this.context, (context, version) => {
917+ if (version != 0) {
918+ debug("Got the stream restore extension");
919+ _pulse_use_stream_restore = true;
920+ } else {
921+ debug("No stream restore extension found");
922+ }
923+
924+ if (_pulse_use_stream_restore) {
925+ this.context.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
926+ PulseAudio.Context.SubscriptionMask.SINK_INPUT |
927+ PulseAudio.Context.SubscriptionMask.SOURCE |
928+ PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
929+ } else {
930+ this.context.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
931+ PulseAudio.Context.SubscriptionMask.SOURCE |
932+ PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT);
933+ }
934+ this.context.set_subscribe_callback (context_events_cb);
935+ this.ready = true;
936+
937+ debug("Pulse state ready");
938+
939+ update_sink ();
940+ update_sink_input ();
941+ update_source ();
942+ update_stream ();
943+ });
944+
945 break;
946
947 case Context.State.FAILED:
948 case Context.State.TERMINATED:
949+ warning("Pulse state disconnected, retrying.");
950+ this.ready = false;
951 if (_reconnect_timer == 0)
952- _reconnect_timer = Timeout.add_seconds (2, reconnect_timeout);
953+ _reconnect_timer = Timeout.add_seconds (2, () => {
954+ _reconnect_timer = 0;
955+ reconnect_to_pulse ();
956+ return false; // G_SOURCE_REMOVE
957+ });
958+ break;
959+
960+ case Context.State.CONNECTING:
961+ case Context.State.AUTHORIZING:
962+ case Context.State.SETTING_NAME:
963+ debug(@"Pulse state initializing step $((int)c.get_state())");
964 break;
965
966 default:
967- this.ready = false;
968+ warning("Unknown state returned by Pulse Audio context. Not reconnecting.");
969 break;
970 }
971 }
972
973- bool reconnect_timeout ()
974- {
975- _reconnect_timer = 0;
976- reconnect_to_pulse ();
977- return false; // G_SOURCE_REMOVE
978- }
979-
980 void reconnect_to_pulse ()
981 {
982 if (this.ready) {
983 this.context.disconnect ();
984 this.context = null;
985 this.ready = false;
986+ this._pulse_use_stream_restore = false;
987 }
988
989 var props = new Proplist ();
990@@ -447,8 +396,6 @@
991 props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control");
992 props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1");
993
994- reconnect_pulse_dbus ();
995-
996 this.context = new PulseAudio.Context (loop.get_api(), null, props);
997 this.context.set_state_callback (context_state_callback);
998
999@@ -456,41 +403,35 @@
1000 warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno()));
1001 }
1002
1003- void sink_info_list_callback_set_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) {
1004- if (sink != null)
1005- context.set_sink_mute_by_index (sink.index, true, null);
1006- }
1007-
1008- void sink_info_list_callback_unset_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) {
1009- if (sink != null)
1010- context.set_sink_mute_by_index (sink.index, false, null);
1011- }
1012-
1013- /* Mute operations */
1014+ /******************************/
1015+ /* Mute operations */
1016+ /******************************/
1017+
1018 bool set_mute_internal (bool mute)
1019 {
1020- return_val_if_fail (context.get_state () == Context.State.READY, false);
1021+ if (!this.ready)
1022+ return false;
1023
1024 if (_mute != mute) {
1025 if (mute)
1026- context.get_sink_info_list (sink_info_list_callback_set_mute);
1027+ context.get_sink_info_list ((context, sink, eol) => {
1028+ if (sink != null)
1029+ context.set_sink_mute_by_index (sink.index, true, null);
1030+ });
1031 else
1032- context.get_sink_info_list (sink_info_list_callback_unset_mute);
1033+ context.get_sink_info_list ((context, sink, eol) => {
1034+ if (sink != null)
1035+ context.set_sink_mute_by_index (sink.index, false, null);
1036+ });
1037 return true;
1038 } else {
1039 return false;
1040 }
1041 }
1042
1043- public void set_mute (bool mute)
1044- {
1045- if (set_mute_internal (mute))
1046- sync_mute_to_accountsservice.begin (mute);
1047- }
1048-
1049 public void toggle_mute ()
1050 {
1051- this.set_mute (!this._mute);
1052+ this.mute = !this._mute;
1053 }
1054
1055 public bool mute
1056@@ -499,17 +440,17 @@
1057 {
1058 return this._mute;
1059 }
1060- }
1061-
1062- public bool is_playing
1063- {
1064- get
1065+ set
1066 {
1067- return this._is_playing;
1068+ if (set_mute_internal (value))
1069+ sync_mute_to_accountsservice.begin (value);
1070 }
1071 }
1072
1073- /* Volume operations */
1074+ /******************************/
1075+ /* Volume operations */
1076+ /******************************/
1077+
1078 private static PulseAudio.Volume double_to_volume (double vol)
1079 {
1080 double tmp = (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED) * vol;
1081@@ -522,79 +463,41 @@
1082 return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED);
1083 }
1084
1085- private void set_volume_success_cb (Context c, int success)
1086- {
1087- if ((bool)success)
1088- this.notify_property("volume");
1089- }
1090-
1091- private void sink_info_set_volume_cb (Context c, SinkInfo? i, int eol)
1092- {
1093- if (i == null)
1094- return;
1095-
1096- unowned CVolume cvol = i.volume;
1097- cvol.scale (double_to_volume (_volume));
1098- c.set_sink_volume_by_index (i.index, cvol, set_volume_success_cb);
1099- }
1100-
1101- private void server_info_cb_for_set_volume (Context c, ServerInfo? i)
1102- {
1103- if (i == null)
1104- {
1105- warning ("Could not get PulseAudio server info");
1106- return;
1107- }
1108-
1109- context.get_sink_info_by_name (i.default_sink_name, sink_info_set_volume_cb);
1110- }
1111-
1112- private async void set_volume_active_role ()
1113- {
1114- string active_role_objp = _objp_role_alert;
1115-
1116- if (_active_sink_input != -1 && _active_sink_input in _sink_input_list)
1117- active_role_objp = _sink_input_hash.get (_active_sink_input);
1118-
1119- try {
1120- var builder = new VariantBuilder (new VariantType ("a(uu)"));
1121- builder.add ("(uu)", 0, double_to_volume (_volume));
1122- Variant volume = builder.end ();
1123-
1124- /* Increase the signal counter so we can handle the callback */
1125- lock (_pa_volume_sig_count) {
1126- _pa_volume_sig_count++;
1127- }
1128-
1129- yield _pconn.call ("org.PulseAudio.Ext.StreamRestore1.RestoreEntry",
1130- active_role_objp, "org.freedesktop.DBus.Properties", "Set",
1131- new Variant ("(ssv)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume", volume),
1132- null, DBusCallFlags.NONE, -1);
1133-
1134- this.notify_property("volume");
1135- } catch (GLib.Error e) {
1136- lock (_pa_volume_sig_count) {
1137- _pa_volume_sig_count--;
1138- }
1139- warning ("unable to set volume for stream obj path %s (%s)", active_role_objp, e.message);
1140- }
1141- }
1142-
1143- bool set_volume_internal (double volume)
1144- {
1145- if (context.get_state () != Context.State.READY)
1146+ bool set_volume_internal (double newvolume)
1147+ {
1148+ if (!this.ready)
1149 return false;
1150
1151- if (_volume != volume) {
1152+ if (_volume != newvolume) {
1153 var old_high_volume = this.high_volume;
1154-
1155- _volume = volume;
1156- if (_pulse_use_stream_restore)
1157- set_volume_active_role.begin ();
1158- else
1159- context.get_server_info (server_info_cb_for_set_volume);
1160-
1161- this.notify_property("volume");
1162+ /* Ideally, we'd be able to set this when we give the value to Pulse, but
1163+ we kinda have a Vala bug that's messing up capturing the variables. So
1164+ we're setting it here so that the VolumeControl object can track it all
1165+ the way through */
1166+ /* https://bugzilla.gnome.org/show_bug.cgi?id=741485 */
1167+ _volume = newvolume;
1168+
1169+ context.get_server_info ((context, i) => {
1170+ if (i == null)
1171+ {
1172+ warning ("Could not get PulseAudio server info");
1173+ return;
1174+ }
1175+
1176+ context.get_sink_info_by_name (i.default_sink_name, (context, info, eol) => {
1177+ if (info == null)
1178+ return;
1179+
1180+ unowned CVolume cvol = info.volume;
1181+ cvol.scale (double_to_volume (this._volume));
1182+ context.set_sink_volume_by_index (info.index, cvol, (context, success) => {
1183+ if (!(bool)success)
1184+ return;
1185+
1186+ this.notify_property("volume");
1187+ });
1188+ });
1189+ });
1190
1191 if (this.high_volume != old_high_volume)
1192 this.notify_property("high-volume");
1193@@ -605,20 +508,6 @@
1194 }
1195 }
1196
1197- void set_mic_volume_success_cb (Context c, int success)
1198- {
1199- if ((bool)success)
1200- this.notify_property ("mic-volume");
1201- }
1202-
1203- void set_mic_volume_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) {
1204- if (i != null) {
1205- unowned CVolume cvol = CVolume ();
1206- cvol = vol_set (cvol, 1, double_to_volume (_mic_volume));
1207- c.set_source_volume_by_name (i.default_source_name, cvol, set_mic_volume_success_cb);
1208- }
1209- }
1210-
1211 public double volume {
1212 get {
1213 return _volume;
1214@@ -635,94 +524,34 @@
1215 return _mic_volume;
1216 }
1217 set {
1218- return_if_fail (context.get_state () == Context.State.READY);
1219+ if (!this.ready)
1220+ return;
1221
1222+ /* Ideally, we'd be able to set this when we give the value to Pulse, but
1223+ we kinda have a Vala bug that's messing up capturing the variables. So
1224+ we're setting it here so that the VolumeControl object can track it all
1225+ the way through */
1226+ /* https://bugzilla.gnome.org/show_bug.cgi?id=741485 */
1227 _mic_volume = value;
1228
1229- context.get_server_info (set_mic_volume_get_server_info_cb);
1230- }
1231- }
1232-
1233- /* PulseAudio Dbus (Stream Restore) logic */
1234- private void reconnect_pulse_dbus ()
1235- {
1236- unowned string pulse_dbus_server_env = Environment.get_variable ("PULSE_DBUS_SERVER");
1237- string address;
1238-
1239- /* In case of a reconnect */
1240- _pulse_use_stream_restore = false;
1241- _pa_volume_sig_count = 0;
1242-
1243- if (pulse_dbus_server_env != null) {
1244- address = pulse_dbus_server_env;
1245- } else {
1246- DBusConnection conn;
1247- Variant props;
1248-
1249- try {
1250- conn = Bus.get_sync (BusType.SESSION);
1251- } catch (GLib.IOError e) {
1252- warning ("unable to get the dbus session bus: %s", e.message);
1253- return;
1254- }
1255-
1256- try {
1257- var props_variant = conn.call_sync ("org.PulseAudio1",
1258- "/org/pulseaudio/server_lookup1", "org.freedesktop.DBus.Properties",
1259- "Get", new Variant ("(ss)", "org.PulseAudio.ServerLookup1", "Address"),
1260- null, DBusCallFlags.NONE, -1);
1261- props_variant.get ("(v)", out props);
1262- address = props.get_string ();
1263- } catch (GLib.Error e) {
1264- warning ("unable to get pulse unix socket: %s", e.message);
1265- return;
1266- }
1267- }
1268-
1269- stdout.printf ("PulseAudio dbus unix socket: %s\n", address);
1270- try {
1271- _pconn = new DBusConnection.for_address_sync (address, DBusConnectionFlags.AUTHENTICATION_CLIENT);
1272- } catch (GLib.Error e) {
1273- /* If it fails, it means the dbus pulse extension is not available */
1274- return;
1275- }
1276-
1277- /* For pulse dbus related events */
1278- _pconn.add_filter (pulse_dbus_filter);
1279-
1280- /* Check if the 4 currently supported media roles are already available in StreamRestore
1281- * Roles: multimedia, alert, alarm and phone */
1282- _objp_role_multimedia = stream_restore_get_object_path ("sink-input-by-media-role:multimedia");
1283- _objp_role_alert = stream_restore_get_object_path ("sink-input-by-media-role:alert");
1284- _objp_role_alarm = stream_restore_get_object_path ("sink-input-by-media-role:alarm");
1285- _objp_role_phone = stream_restore_get_object_path ("sink-input-by-media-role:phone");
1286-
1287- /* Only use stream restore if every used role is available */
1288- if (_objp_role_multimedia != null && _objp_role_alert != null && _objp_role_alarm != null && _objp_role_phone != null) {
1289- stdout.printf ("Using PulseAudio DBUS Stream Restore module\n");
1290- /* Restore volume and update default entry */
1291- update_active_sink_input.begin (-1);
1292- _pulse_use_stream_restore = true;
1293- }
1294- }
1295-
1296- private string? stream_restore_get_object_path (string name) {
1297- string? objp = null;
1298- try {
1299- Variant props_variant = _pconn.call_sync ("org.PulseAudio.Ext.StreamRestore1",
1300- "/org/pulseaudio/stream_restore1", "org.PulseAudio.Ext.StreamRestore1",
1301- "GetEntryByName", new Variant ("(s)", name), null, DBusCallFlags.NONE, -1);
1302- /* Workaround for older versions of vala that don't provide get_objv */
1303- VariantIter iter = props_variant.iterator ();
1304- iter.next ("o", &objp);
1305- stdout.printf ("Found obj path %s for restore data named %s\n", objp, name);
1306- } catch (GLib.Error e) {
1307- warning ("unable to find stream restore data for: %s", name);
1308- }
1309- return objp;
1310- }
1311-
1312+ context.get_server_info ((context, info) => {
1313+ if (info == null)
1314+ return;
1315+
1316+ unowned CVolume cvol = CVolume ();
1317+ cvol = vol_set (cvol, 1, double_to_volume (_mic_volume));
1318+ context.set_source_volume_by_name (info.default_source_name, cvol, (context, success) => {
1319+ if ((bool)success)
1320+ this.notify_property ("mic-volume");
1321+ });
1322+ });
1323+ }
1324+ }
1325+
1326+ /******************************/
1327 /* AccountsService operations */
1328+ /******************************/
1329+
1330 private void accountsservice_props_changed_cb (DBusProxy proxy, Variant changed_properties, string[]? invalidated_properties)
1331 {
1332 Variant volume_variant = changed_properties.lookup_value ("Volume", new VariantType ("d"));
1333
1334=== modified file 'tests/CMakeLists.txt'
1335--- tests/CMakeLists.txt 2015-01-28 15:41:22 +0000
1336+++ tests/CMakeLists.txt 2015-01-30 23:01:09 +0000
1337@@ -41,6 +41,9 @@
1338 vala_add(vala-mocks
1339 media-player-mock.vala
1340 )
1341+vala_add(vala-mocks
1342+ focus-tracker-mock.vala
1343+)
1344
1345 vala_finish(vala-mocks
1346 SOURCES
1347@@ -92,6 +95,8 @@
1348 pa-mock.cpp
1349 )
1350
1351+target_link_libraries (pulse-mock ${PULSEAUDIO_LIBRARIES})
1352+
1353 ###########################
1354 # Name Watch Test
1355 ###########################
1356@@ -135,6 +140,7 @@
1357 volume-control-test
1358 indicator-sound-service-lib
1359 pulse-mock
1360+ vala-mocks-lib
1361 gtest
1362 ${TEST_LIBRARIES}
1363 )
1364@@ -207,3 +213,29 @@
1365 greeter-list-test --gtest_filter=GreeterListTest.BasicIterator
1366 )
1367
1368+###########################
1369+# Indicator Test
1370+###########################
1371+
1372+add_definitions(
1373+ -DINDICATOR_SOUND_SERVICE_BINARY="${CMAKE_BINARY_DIR}/src/indicator-sound-service"
1374+ -DPA_MOCK_LIB="${CMAKE_CURRENT_BINARY_DIR}/libpulse-mock.so"
1375+)
1376+add_executable (indicator-test indicator-test.cc)
1377+target_link_libraries (
1378+ indicator-test
1379+ gtest
1380+ ${SOUNDSERVICE_LIBRARIES}
1381+ ${TEST_LIBRARIES}
1382+)
1383+
1384+# Split tests to work around libaccountservice sucking
1385+add_test(indcator-test-phone-menu
1386+ indicator-test --gtest_filter=IndicatorTest.PhoneMenu
1387+)
1388+add_test(indcator-test-desktop-menu
1389+ indicator-test --gtest_filter=IndicatorTest.DesktopMenu
1390+)
1391+add_test(indcator-test-silent-actions
1392+ indicator-test --gtest_filter=IndicatorTest.SilentActions
1393+)
1394
1395=== modified file 'tests/accounts-service-mock.h'
1396--- tests/accounts-service-mock.h 2014-03-04 21:24:10 +0000
1397+++ tests/accounts-service-mock.h 2015-01-30 23:01:09 +0000
1398@@ -17,6 +17,7 @@
1399 * Ted Gould <ted@canonical.com>
1400 */
1401
1402+#include <memory>
1403 #include <libdbustest/dbus-test.h>
1404
1405 class AccountsServiceMock
1406@@ -24,11 +25,14 @@
1407 DbusTestDbusMock * mock = nullptr;
1408 DbusTestDbusMockObject * soundobj = nullptr;
1409 DbusTestDbusMockObject * userobj = nullptr;
1410+ DbusTestDbusMockObject * syssoundobj = nullptr;
1411
1412 public:
1413 AccountsServiceMock () {
1414 mock = dbus_test_dbus_mock_new("org.freedesktop.Accounts");
1415
1416+ dbus_test_task_set_bus(DBUS_TEST_TASK(mock), DBUS_TEST_SERVICE_BUS_SYSTEM);
1417+
1418 DbusTestDbusMockObject * baseobj = dbus_test_dbus_mock_get_object(mock, "/org/freedesktop/Accounts", "org.freedesktop.Accounts", NULL);
1419
1420 dbus_test_dbus_mock_object_add_method(mock, baseobj,
1421@@ -80,6 +84,11 @@
1422 dbus_test_dbus_mock_object_add_property(mock, soundobj,
1423 "ArtUrl", G_VARIANT_TYPE_STRING,
1424 g_variant_new_string(""), NULL);
1425+
1426+ syssoundobj = dbus_test_dbus_mock_get_object(mock, "/user", "com.ubuntu.touch.AccountsService.Sound", NULL);
1427+ dbus_test_dbus_mock_object_add_property(mock, syssoundobj,
1428+ "SilentMode", G_VARIANT_TYPE_BOOLEAN,
1429+ g_variant_new_boolean(FALSE), NULL);
1430 }
1431
1432 ~AccountsServiceMock () {
1433@@ -87,6 +96,18 @@
1434 g_clear_object(&mock);
1435 }
1436
1437+ void setSilentMode (bool modeValue) {
1438+ dbus_test_dbus_mock_object_update_property(mock, syssoundobj,
1439+ "SilentMode", g_variant_new_boolean(modeValue ? TRUE : FALSE),
1440+ NULL);
1441+ }
1442+
1443+ operator std::shared_ptr<DbusTestTask> () {
1444+ return std::shared_ptr<DbusTestTask>(
1445+ DBUS_TEST_TASK(g_object_ref(mock)),
1446+ [](DbusTestTask * task) { g_clear_object(&task); });
1447+ }
1448+
1449 operator DbusTestTask* () {
1450 return DBUS_TEST_TASK(mock);
1451 }
1452
1453=== modified file 'tests/accounts-service-user.cc'
1454--- tests/accounts-service-user.cc 2014-03-03 20:50:52 +0000
1455+++ tests/accounts-service-user.cc 2015-01-30 23:01:09 +0000
1456@@ -42,14 +42,13 @@
1457
1458 virtual void SetUp() {
1459 service = dbus_test_service_new(NULL);
1460+ dbus_test_service_set_bus(service, DBUS_TEST_SERVICE_BUS_BOTH);
1461
1462 AccountsServiceMock service_mock;
1463
1464 dbus_test_service_add_task(service, (DbusTestTask*)service_mock);
1465 dbus_test_service_start_tasks(service);
1466
1467- g_setenv("DBUS_SYSTEM_BUS_ADDRESS", g_getenv("DBUS_SESSION_BUS_ADDRESS"), TRUE);
1468-
1469 session = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
1470 ASSERT_NE(nullptr, session);
1471 g_dbus_connection_set_exit_on_close(session, FALSE);
1472
1473=== added file 'tests/focus-tracker-mock.vala'
1474--- tests/focus-tracker-mock.vala 1970-01-01 00:00:00 +0000
1475+++ tests/focus-tracker-mock.vala 2015-01-30 23:01:09 +0000
1476@@ -0,0 +1,27 @@
1477+/*
1478+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
1479+ * Copyright © 2015 Canonical Ltd.
1480+ *
1481+ * This program is free software; you can redistribute it and/or modify
1482+ * it under the terms of the GNU General Public License as published by
1483+ * the Free Software Foundation; version 3.
1484+ *
1485+ * This program is distributed in the hope that it will be useful,
1486+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1487+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1488+ * GNU General Public License for more details.
1489+ *
1490+ * You should have received a copy of the GNU General Public License
1491+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1492+ *
1493+ * Authors:
1494+ * Ted Gould <ted@canonical.com>
1495+ */
1496+
1497+public class FocusTrackerMock : FocusTracker {
1498+ public override string focused_appid { get; private set; default = "unknown"; }
1499+
1500+ public void mock_set_appid (string newappid) {
1501+ this.focused_appid = newappid;
1502+ }
1503+}
1504
1505=== added file 'tests/indicator-fixture.h'
1506--- tests/indicator-fixture.h 1970-01-01 00:00:00 +0000
1507+++ tests/indicator-fixture.h 2015-01-30 23:01:09 +0000
1508@@ -0,0 +1,444 @@
1509+/*
1510+ * Copyright © 2014 Canonical Ltd.
1511+ *
1512+ * This program is free software; you can redistribute it and/or modify
1513+ * it under the terms of the GNU General Public License as published by
1514+ * the Free Software Foundation; version 3.
1515+ *
1516+ * This program is distributed in the hope that it will be useful,
1517+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1518+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1519+ * GNU General Public License for more details.
1520+ *
1521+ * You should have received a copy of the GNU General Public License
1522+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1523+ *
1524+ * Authors:
1525+ * Ted Gould <ted@canonical.com>
1526+ */
1527+
1528+#include <memory>
1529+#include <algorithm>
1530+#include <string>
1531+
1532+#include <gtest/gtest.h>
1533+#include <gio/gio.h>
1534+#include <libdbustest/dbus-test.h>
1535+
1536+class IndicatorFixture : public ::testing::Test
1537+{
1538+ private:
1539+ std::string _indicatorPath;
1540+ std::string _indicatorAddress;
1541+ std::vector<std::shared_ptr<DbusTestTask>> _mocks;
1542+
1543+ class PerRunData {
1544+ public:
1545+ /* We're private in the fixture but other than that we don't care,
1546+ we don't leak out. This object's purpose isn't to hide data it is
1547+ to make the lifecycle of the items more clear. */
1548+ std::shared_ptr<GMenuModel> _menu;
1549+ std::shared_ptr<GActionGroup> _actions;
1550+ DbusTestService * _session_service;
1551+ DbusTestService * _system_service;
1552+ DbusTestTask * _test_indicator;
1553+ DbusTestTask * _test_dummy;
1554+ GDBusConnection * _session;
1555+ GDBusConnection * _system;
1556+
1557+ PerRunData (const std::string& indicatorPath, const std::string& indicatorAddress, std::vector<std::shared_ptr<DbusTestTask>>& mocks)
1558+ : _menu(nullptr)
1559+ , _session(nullptr)
1560+ {
1561+ _session_service = dbus_test_service_new(nullptr);
1562+ dbus_test_service_set_bus(_session_service, DBUS_TEST_SERVICE_BUS_SESSION);
1563+
1564+ _system_service = dbus_test_service_new(nullptr);
1565+ dbus_test_service_set_bus(_system_service, DBUS_TEST_SERVICE_BUS_SYSTEM);
1566+
1567+ _test_indicator = DBUS_TEST_TASK(dbus_test_process_new(indicatorPath.c_str()));
1568+ dbus_test_task_set_name(_test_indicator, "Indicator");
1569+ dbus_test_service_add_task(_session_service, _test_indicator);
1570+
1571+ _test_dummy = dbus_test_task_new();
1572+ dbus_test_task_set_wait_for(_test_dummy, indicatorAddress.c_str());
1573+ dbus_test_task_set_name(_test_dummy, "Dummy");
1574+ dbus_test_service_add_task(_session_service, _test_dummy);
1575+
1576+ std::for_each(mocks.begin(), mocks.end(), [this](std::shared_ptr<DbusTestTask> task) {
1577+ if (dbus_test_task_get_bus(task.get()) == DBUS_TEST_SERVICE_BUS_SYSTEM) {
1578+ dbus_test_service_add_task(_system_service, task.get());
1579+ } else {
1580+ dbus_test_service_add_task(_session_service, task.get());
1581+ }
1582+ });
1583+
1584+ g_debug("Starting System Bus");
1585+ dbus_test_service_start_tasks(_system_service);
1586+ _system = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, nullptr);
1587+ g_dbus_connection_set_exit_on_close(_system, FALSE);
1588+
1589+ g_debug("Starting Session Bus");
1590+ dbus_test_service_start_tasks(_session_service);
1591+ _session = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
1592+ g_dbus_connection_set_exit_on_close(_session, FALSE);
1593+ }
1594+
1595+ virtual ~PerRunData (void) {
1596+ _menu.reset();
1597+ _actions.reset();
1598+
1599+ /* D-Bus Test Stuff */
1600+ g_clear_object(&_test_dummy);
1601+ g_clear_object(&_test_indicator);
1602+ g_clear_object(&_session_service);
1603+ g_clear_object(&_system_service);
1604+
1605+ /* Wait for D-Bus session bus to go */
1606+ if (!g_dbus_connection_is_closed(_session)) {
1607+ g_dbus_connection_close_sync(_session, nullptr, nullptr);
1608+ }
1609+ g_clear_object(&_session);
1610+
1611+ if (!g_dbus_connection_is_closed(_system)) {
1612+ g_dbus_connection_close_sync(_system, nullptr, nullptr);
1613+ }
1614+ g_clear_object(&_system);
1615+ }
1616+ };
1617+
1618+ std::shared_ptr<PerRunData> run;
1619+
1620+ public:
1621+ virtual ~IndicatorFixture() = default;
1622+
1623+ IndicatorFixture (const std::string& path,
1624+ const std::string& addr)
1625+ : _indicatorPath(path)
1626+ , _indicatorAddress(addr)
1627+ {
1628+ };
1629+
1630+
1631+ protected:
1632+ virtual void SetUp() override
1633+ {
1634+ run = std::make_shared<PerRunData>(_indicatorPath, _indicatorAddress, _mocks);
1635+
1636+ _mocks.clear();
1637+ }
1638+
1639+ virtual void TearDown() override
1640+ {
1641+ run.reset();
1642+ }
1643+
1644+ void addMock (std::shared_ptr<DbusTestTask> mock)
1645+ {
1646+ _mocks.push_back(mock);
1647+ }
1648+
1649+ std::shared_ptr<DbusTestTask> buildBustleMock (const std::string& filename, DbusTestServiceBus bus = DBUS_TEST_SERVICE_BUS_BOTH)
1650+ {
1651+ return std::shared_ptr<DbusTestTask>([filename, bus]() {
1652+ DbusTestTask * bustle = DBUS_TEST_TASK(dbus_test_bustle_new(filename.c_str()));
1653+ dbus_test_task_set_name(bustle, "Bustle");
1654+ dbus_test_task_set_bus(bustle, bus);
1655+ return bustle;
1656+ }(), [](DbusTestTask * bustle) {
1657+ g_clear_object(&bustle);
1658+ });
1659+ }
1660+
1661+ private:
1662+ void waitForCore (GObject * obj, const gchar * signalname) {
1663+ auto loop = g_main_loop_new(nullptr, FALSE);
1664+
1665+ /* Our two exit criteria */
1666+ gulong signal = g_signal_connect_swapped(obj, signalname, G_CALLBACK(g_main_loop_quit), loop);
1667+ guint timer = g_timeout_add_seconds(5, [](gpointer user_data) -> gboolean {
1668+ g_warning("Menu Timeout");
1669+ g_main_loop_quit((GMainLoop *)user_data);
1670+ return G_SOURCE_CONTINUE;
1671+ }, loop);
1672+
1673+ /* Wait for sync */
1674+ g_main_loop_run(loop);
1675+
1676+ /* Clean up */
1677+ g_source_remove(timer);
1678+ g_signal_handler_disconnect(obj, signal);
1679+
1680+ g_main_loop_unref(loop);
1681+ }
1682+
1683+ void menuWaitForItems (const std::shared_ptr<GMenuModel>& menu) {
1684+ auto count = g_menu_model_get_n_items(menu.get());
1685+
1686+ if (count != 0)
1687+ return;
1688+
1689+ waitForCore(G_OBJECT(menu.get()), "items-changed");
1690+ }
1691+
1692+ void agWaitForActions (const std::shared_ptr<GActionGroup>& group) {
1693+ auto list = std::shared_ptr<gchar *>(
1694+ g_action_group_list_actions(group.get()),
1695+ [](gchar ** list) {
1696+ g_strfreev(list);
1697+ });
1698+
1699+ if (g_strv_length(list.get()) != 0) {
1700+ return;
1701+ }
1702+
1703+ waitForCore(G_OBJECT(group.get()), "action-added");
1704+ }
1705+
1706+ protected:
1707+ void setMenu (const std::string& path) {
1708+ run->_menu.reset();
1709+
1710+ g_debug("Getting Menu: %s:%s", _indicatorAddress.c_str(), path.c_str());
1711+ run->_menu = std::shared_ptr<GMenuModel>(G_MENU_MODEL(g_dbus_menu_model_get(run->_session, _indicatorAddress.c_str(), path.c_str())), [](GMenuModel * modelptr) {
1712+ g_clear_object(&modelptr);
1713+ });
1714+
1715+ menuWaitForItems(run->_menu);
1716+ }
1717+
1718+ void setActions (const std::string& path) {
1719+ run->_actions.reset();
1720+
1721+ run->_actions = std::shared_ptr<GActionGroup>(G_ACTION_GROUP(g_dbus_action_group_get(run->_session, _indicatorAddress.c_str(), path.c_str())), [](GActionGroup * groupptr) {
1722+ g_clear_object(&groupptr);
1723+ });
1724+
1725+ agWaitForActions(run->_actions);
1726+ }
1727+
1728+ testing::AssertionResult expectActionExists (const gchar * nameStr, const std::string& name) {
1729+ bool hasit = g_action_group_has_action(run->_actions.get(), name.c_str());
1730+
1731+ if (!hasit) {
1732+ auto result = testing::AssertionFailure();
1733+ result <<
1734+ " Action: " << nameStr << std::endl <<
1735+ " Expected: " << "Exists" << std::endl <<
1736+ " Actual: " << "No action found" << std::endl;
1737+
1738+ return result;
1739+ }
1740+
1741+ auto result = testing::AssertionSuccess();
1742+ return result;
1743+ }
1744+
1745+ testing::AssertionResult expectActionStateType (const char * nameStr, const char * typeStr, const std::string& name, const GVariantType * type) {
1746+ auto atype = g_action_group_get_action_state_type(run->_actions.get(), name.c_str());
1747+ bool same = false;
1748+
1749+ if (atype != nullptr) {
1750+ same = g_variant_type_equal(atype, type);
1751+ }
1752+
1753+ if (!same) {
1754+ auto result = testing::AssertionFailure();
1755+ result <<
1756+ " Action: " << nameStr << std::endl <<
1757+ " Expected: " << typeStr << std::endl <<
1758+ " Actual: " << g_variant_type_peek_string(atype) << std::endl;
1759+
1760+ return result;
1761+ }
1762+
1763+ auto result = testing::AssertionSuccess();
1764+ return result;
1765+ }
1766+
1767+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, GVariant * value) {
1768+ auto varref = std::shared_ptr<GVariant>(g_variant_ref_sink(value), [](GVariant * varptr) {
1769+ if (varptr != nullptr)
1770+ g_variant_unref(varptr);
1771+ });
1772+ auto aval = std::shared_ptr<GVariant>(g_action_group_get_action_state(run->_actions.get(), name.c_str()), [] (GVariant * varptr) {
1773+ if (varptr != nullptr)
1774+ g_variant_unref(varptr);
1775+ });
1776+ bool match = false;
1777+
1778+ if (aval != nullptr) {
1779+ match = g_variant_equal(aval.get(), varref.get());
1780+ }
1781+
1782+ if (!match) {
1783+ gchar * attstr = nullptr;
1784+
1785+ if (aval != nullptr) {
1786+ attstr = g_variant_print(aval.get(), TRUE);
1787+ } else {
1788+ attstr = g_strdup("nullptr");
1789+ }
1790+
1791+ auto result = testing::AssertionFailure();
1792+ result <<
1793+ " Action: " << nameStr << std::endl <<
1794+ " Expected: " << valueStr << std::endl <<
1795+ " Actual: " << attstr << std::endl;
1796+
1797+ g_free(attstr);
1798+
1799+ return result;
1800+ } else {
1801+ auto result = testing::AssertionSuccess();
1802+ return result;
1803+ }
1804+ }
1805+
1806+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, bool value) {
1807+ GVariant * var = g_variant_new_boolean(value);
1808+ return expectActionStateIs(nameStr, valueStr, name, var);
1809+ }
1810+
1811+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, std::string value) {
1812+ GVariant * var = g_variant_new_string(value.c_str());
1813+ return expectActionStateIs(nameStr, valueStr, name, var);
1814+ }
1815+
1816+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, const char * value) {
1817+ GVariant * var = g_variant_new_string(value);
1818+ return expectActionStateIs(nameStr, valueStr, name, var);
1819+ }
1820+
1821+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, double value) {
1822+ GVariant * var = g_variant_new_double(value);
1823+ return expectActionStateIs(nameStr, valueStr, name, var);
1824+ }
1825+
1826+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, float value) {
1827+ GVariant * var = g_variant_new_double(value);
1828+ return expectActionStateIs(nameStr, valueStr, name, var);
1829+ }
1830+
1831+
1832+ private:
1833+ std::shared_ptr<GVariant> getMenuAttributeVal (int location, std::shared_ptr<GMenuModel>& menu, const std::string& attribute, std::shared_ptr<GVariant>& value) {
1834+ if (!(location < g_menu_model_get_n_items(menu.get()))) {
1835+ return nullptr;
1836+ }
1837+
1838+ if (location >= g_menu_model_get_n_items(menu.get()))
1839+ return nullptr;
1840+
1841+ auto menuval = std::shared_ptr<GVariant>(g_menu_model_get_item_attribute_value(menu.get(), location, attribute.c_str(), g_variant_get_type(value.get())), [](GVariant * varptr) {
1842+ if (varptr != nullptr)
1843+ g_variant_unref(varptr);
1844+ });
1845+
1846+ return menuval;
1847+ }
1848+
1849+ std::shared_ptr<GVariant> getMenuAttributeRecurse (std::vector<int>::const_iterator menuLocation, std::vector<int>::const_iterator menuEnd, const std::string& attribute, std::shared_ptr<GVariant>& value, std::shared_ptr<GMenuModel>& menu) {
1850+ if (menuLocation == menuEnd)
1851+ return nullptr;
1852+
1853+ if (menuLocation + 1 == menuEnd)
1854+ return getMenuAttributeVal(*menuLocation, menu, attribute, value);
1855+
1856+ auto clearfunc = [](GMenuModel * modelptr) {
1857+ g_clear_object(&modelptr);
1858+ };
1859+
1860+ auto submenu = std::shared_ptr<GMenuModel>(g_menu_model_get_item_link(menu.get(), *menuLocation, G_MENU_LINK_SUBMENU), clearfunc);
1861+
1862+ if (submenu == nullptr)
1863+ submenu = std::shared_ptr<GMenuModel>(g_menu_model_get_item_link(menu.get(), *menuLocation, G_MENU_LINK_SECTION), clearfunc);
1864+
1865+ if (submenu == nullptr)
1866+ return nullptr;
1867+
1868+ menuWaitForItems(submenu);
1869+
1870+ return getMenuAttributeRecurse(menuLocation + 1, menuEnd, attribute, value, submenu);
1871+ }
1872+
1873+ protected:
1874+ testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const gchar * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, GVariant * value) {
1875+ auto varref = std::shared_ptr<GVariant>(g_variant_ref_sink(value), [](GVariant * varptr) {
1876+ if (varptr != nullptr)
1877+ g_variant_unref(varptr);
1878+ });
1879+
1880+ auto attrib = getMenuAttributeRecurse(menuLocation.cbegin(), menuLocation.cend(), attribute, varref, run->_menu);
1881+ bool same = false;
1882+
1883+ if (attrib != nullptr && varref != nullptr) {
1884+ same = g_variant_equal(attrib.get(), varref.get());
1885+ }
1886+
1887+ if (!same) {
1888+ gchar * attstr = nullptr;
1889+
1890+ if (attrib != nullptr) {
1891+ attstr = g_variant_print(attrib.get(), TRUE);
1892+ } else {
1893+ attstr = g_strdup("nullptr");
1894+ }
1895+
1896+ auto result = testing::AssertionFailure();
1897+ result <<
1898+ " Menu: " << menuLocationStr << std::endl <<
1899+ " Attribute: " << attributeStr << std::endl <<
1900+ " Expected: " << valueStr << std::endl <<
1901+ " Actual: " << attstr << std::endl;
1902+
1903+ g_free(attstr);
1904+
1905+ return result;
1906+ } else {
1907+ auto result = testing::AssertionSuccess();
1908+ return result;
1909+ }
1910+ }
1911+
1912+ testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const gchar * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, bool value) {
1913+ GVariant * var = g_variant_new_boolean(value);
1914+ return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var);
1915+ }
1916+
1917+ testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const gchar * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, std::string value) {
1918+ GVariant * var = g_variant_new_string(value.c_str());
1919+ return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var);
1920+ }
1921+
1922+ testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const gchar * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, const char * value) {
1923+ GVariant * var = g_variant_new_string(value);
1924+ return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var);
1925+ }
1926+
1927+};
1928+
1929+#define EXPECT_MENU_ATTRIB(menu, attrib, value) \
1930+ EXPECT_PRED_FORMAT3(IndicatorFixture::expectMenuAttribute, menu, attrib, value)
1931+
1932+#define ASSERT_MENU_ATTRIB(menu, attrib, value) \
1933+ ASSERT_PRED_FORMAT3(IndicatorFixture::expectMenuAttribute, menu, attrib, value)
1934+
1935+#define ASSERT_ACTION_EXISTS(action) \
1936+ ASSERT_PRED_FORMAT1(IndicatorFixture::expectActionExists, action)
1937+
1938+#define EXPECT_ACTION_EXISTS(action) \
1939+ EXPECT_PRED_FORMAT1(IndicatorFixture::expectActionExists, action)
1940+
1941+#define EXPECT_ACTION_STATE(action, value) \
1942+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionStateIs, action, value)
1943+
1944+#define ASSERT_ACTION_STATE(action, value) \
1945+ ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionStateIs, action, value)
1946+
1947+#define EXPECT_ACTION_STATE_TYPE(action, type) \
1948+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionStateType, action, type)
1949+
1950+#define ASSERT_ACTION_STATE_TYPE(action, type) \
1951+ ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionStateType, action, type)
1952+
1953
1954=== added file 'tests/indicator-test.cc'
1955--- tests/indicator-test.cc 1970-01-01 00:00:00 +0000
1956+++ tests/indicator-test.cc 2015-01-30 23:01:09 +0000
1957@@ -0,0 +1,110 @@
1958+/*
1959+ * Copyright © 2014 Canonical Ltd.
1960+ *
1961+ * This program is free software; you can redistribute it and/or modify
1962+ * it under the terms of the GNU General Public License as published by
1963+ * the Free Software Foundation; version 3.
1964+ *
1965+ * This program is distributed in the hope that it will be useful,
1966+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1967+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1968+ * GNU General Public License for more details.
1969+ *
1970+ * You should have received a copy of the GNU General Public License
1971+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1972+ *
1973+ * Authors:
1974+ * Ted Gould <ted@canonical.com>
1975+ */
1976+
1977+#include <gtest/gtest.h>
1978+#include <gio/gio.h>
1979+
1980+#include "indicator-fixture.h"
1981+#include "accounts-service-mock.h"
1982+
1983+class IndicatorTest : public IndicatorFixture
1984+{
1985+protected:
1986+ IndicatorTest (void) :
1987+ IndicatorFixture(INDICATOR_SOUND_SERVICE_BINARY, "com.canonical.indicator.sound")
1988+ {
1989+ }
1990+
1991+ std::shared_ptr<AccountsServiceMock> as;
1992+
1993+ virtual void SetUp() override
1994+ {
1995+ //addMock(buildBustleMock("indicator-test-session.bustle", DBUS_TEST_SERVICE_BUS_SESSION));
1996+ //addMock(buildBustleMock("indicator-test-system.bustle", DBUS_TEST_SERVICE_BUS_SYSTEM));
1997+ g_setenv("LD_PRELOAD", PA_MOCK_LIB, TRUE);
1998+
1999+ as = std::make_shared<AccountsServiceMock>();
2000+ addMock(*as);
2001+
2002+ IndicatorFixture::SetUp();
2003+ }
2004+
2005+ virtual void TearDown() override
2006+ {
2007+ as.reset();
2008+
2009+ IndicatorFixture::TearDown();
2010+ }
2011+
2012+};
2013+
2014+
2015+TEST_F(IndicatorTest, PhoneMenu) {
2016+ setMenu("/com/canonical/indicator/sound/phone");
2017+
2018+ EXPECT_MENU_ATTRIB({0}, "action", "indicator.root");
2019+ EXPECT_MENU_ATTRIB({0}, "x-canonical-type", "com.canonical.indicator.root");
2020+ EXPECT_MENU_ATTRIB({0}, "x-canonical-scroll-action", "indicator.scroll");
2021+ EXPECT_MENU_ATTRIB({0}, "x-canonical-secondary-action", "indicator.mute");
2022+
2023+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "action", "indicator.silent-mode");
2024+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "label", "Silent Mode");
2025+
2026+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 1}), "action", "indicator.phone-settings");
2027+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 1}), "label", "Sound Settings…");
2028+}
2029+
2030+TEST_F(IndicatorTest, DesktopMenu) {
2031+ setMenu("/com/canonical/indicator/sound/desktop");
2032+
2033+ EXPECT_MENU_ATTRIB({0}, "action", "indicator.root");
2034+ EXPECT_MENU_ATTRIB({0}, "x-canonical-type", "com.canonical.indicator.root");
2035+ EXPECT_MENU_ATTRIB({0}, "x-canonical-scroll-action", "indicator.scroll");
2036+ EXPECT_MENU_ATTRIB({0}, "x-canonical-secondary-action", "indicator.mute");
2037+
2038+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "action", "indicator.mute");
2039+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "label", "Mute");
2040+
2041+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 2}), "action", "indicator.desktop-settings");
2042+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 2}), "label", "Sound Settings…");
2043+}
2044+
2045+TEST_F(IndicatorTest, BaseActions) {
2046+ setActions("/com/canonical/indicator/sound");
2047+
2048+ ASSERT_ACTION_EXISTS("root");
2049+ ASSERT_ACTION_STATE_TYPE("root", G_VARIANT_TYPE("a{sv}"));
2050+
2051+ ASSERT_ACTION_EXISTS("scroll");
2052+
2053+ ASSERT_ACTION_EXISTS("silent-mode");
2054+ ASSERT_ACTION_STATE_TYPE("silent-mode", G_VARIANT_TYPE_BOOLEAN);
2055+ EXPECT_ACTION_STATE("silent-mode", false);
2056+
2057+ ASSERT_ACTION_EXISTS("mute");
2058+ ASSERT_ACTION_STATE_TYPE("mute", G_VARIANT_TYPE_BOOLEAN);
2059+ EXPECT_ACTION_STATE("mute", false);
2060+
2061+ ASSERT_ACTION_EXISTS("mic-volume");
2062+ ASSERT_ACTION_STATE_TYPE("mic-volume", G_VARIANT_TYPE_DOUBLE);
2063+
2064+ ASSERT_ACTION_EXISTS("volume");
2065+ ASSERT_ACTION_STATE_TYPE("volume", G_VARIANT_TYPE_DOUBLE);
2066+}
2067+
2068
2069=== modified file 'tests/media-player-user.cc'
2070--- tests/media-player-user.cc 2014-03-25 15:54:57 +0000
2071+++ tests/media-player-user.cc 2015-01-30 23:01:09 +0000
2072@@ -34,30 +34,22 @@
2073 DbusTestService * service = NULL;
2074 AccountsServiceMock service_mock;
2075
2076- GDBusConnection * session = NULL;
2077 GDBusConnection * system = NULL;
2078 GDBusProxy * proxy = NULL;
2079
2080 virtual void SetUp() {
2081 service = dbus_test_service_new(NULL);
2082-
2083+ dbus_test_service_set_bus(service, DBUS_TEST_SERVICE_BUS_SYSTEM);
2084
2085 dbus_test_service_add_task(service, (DbusTestTask*)service_mock);
2086 dbus_test_service_start_tasks(service);
2087
2088- g_setenv("DBUS_SYSTEM_BUS_ADDRESS", g_getenv("DBUS_SESSION_BUS_ADDRESS"), TRUE);
2089-
2090- session = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
2091- ASSERT_NE(nullptr, session);
2092- g_dbus_connection_set_exit_on_close(session, FALSE);
2093- g_object_add_weak_pointer(G_OBJECT(session), (gpointer *)&session);
2094-
2095 system = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL);
2096 ASSERT_NE(nullptr, system);
2097 g_dbus_connection_set_exit_on_close(system, FALSE);
2098 g_object_add_weak_pointer(G_OBJECT(system), (gpointer *)&system);
2099
2100- proxy = g_dbus_proxy_new_sync(session,
2101+ proxy = g_dbus_proxy_new_sync(system,
2102 G_DBUS_PROXY_FLAGS_NONE,
2103 NULL,
2104 "org.freedesktop.Accounts",
2105@@ -71,7 +63,6 @@
2106 g_clear_object(&proxy);
2107 g_clear_object(&service);
2108
2109- g_object_unref(session);
2110 g_object_unref(system);
2111
2112 #if 0
2113@@ -189,6 +180,9 @@
2114 set_property("Album", g_variant_new_string("Vinyl is dead"));
2115 set_property("ArtUrl", g_variant_new_string("http://art.url"));
2116
2117+ /* Ensure the properties get set before we pull them */
2118+ loop(100);
2119+
2120 /* Build our media player */
2121 MediaPlayerUser * player = media_player_user_new("user");
2122 ASSERT_NE(nullptr, player);
2123
2124=== modified file 'tests/pa-mock.cpp'
2125--- tests/pa-mock.cpp 2015-01-29 14:34:50 +0000
2126+++ tests/pa-mock.cpp 2015-01-30 23:01:09 +0000
2127@@ -23,6 +23,7 @@
2128
2129 #include <pulse/pulseaudio.h>
2130 #include <pulse/glib-mainloop.h>
2131+#include <pulse/ext-stream-restore.h>
2132 #include <gio/gio.h>
2133 #include <math.h>
2134
2135@@ -280,6 +281,12 @@
2136 return dummy_operation();
2137 }
2138
2139+pa_operation *
2140+pa_context_get_sink_input_info_list (pa_context *c, pa_sink_input_info_cb_t cb, void * userdata)
2141+{
2142+ return pa_context_get_sink_input_info (c, 0, cb, userdata);
2143+}
2144+
2145 pa_operation*
2146 pa_context_get_source_info_by_name (pa_context *c, const char * name, pa_source_info_cb_t cb, void *userdata)
2147 {
2148@@ -569,3 +576,18 @@
2149 return cvol;
2150 }
2151
2152+/* *******************************
2153+ * ext-stream-restore.h
2154+ * *******************************/
2155+
2156+pa_operation *
2157+pa_ext_stream_restore_test (pa_context * c, pa_ext_stream_restore_test_cb_t callback, void * userdata)
2158+{
2159+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
2160+ [c, callback, userdata]() {
2161+ if (callback != nullptr)
2162+ callback(c, 1, userdata);
2163+ });
2164+
2165+ return dummy_operation();
2166+}
2167
2168=== modified file 'tests/volume-control-test.cc'
2169--- tests/volume-control-test.cc 2015-01-27 17:42:26 +0000
2170+++ tests/volume-control-test.cc 2015-01-30 23:01:09 +0000
2171@@ -23,6 +23,7 @@
2172
2173 extern "C" {
2174 #include "indicator-sound-service.h"
2175+#include "vala-mocks.h"
2176 }
2177
2178 class VolumeControlTest : public ::testing::Test
2179@@ -71,7 +72,8 @@
2180 };
2181
2182 TEST_F(VolumeControlTest, BasicObject) {
2183- VolumeControl * control = volume_control_new();
2184+ FocusTrackerMock * focustracker = focus_tracker_mock_new();
2185+ VolumeControl * control = volume_control_new(FOCUS_TRACKER(focustracker));
2186
2187 /* Setup the PA backend */
2188 loop(100);
2189
2190=== added file 'vapi/libpulse-ext-stream-restore.vapi'
2191--- vapi/libpulse-ext-stream-restore.vapi 1970-01-01 00:00:00 +0000
2192+++ vapi/libpulse-ext-stream-restore.vapi 2015-01-30 23:01:09 +0000
2193@@ -0,0 +1,22 @@
2194+
2195+using GLib;
2196+using PulseAudio;
2197+
2198+[CCode (cheader_filename="pulse/ext-stream-restore.h")]
2199+namespace PulseAudio.Extension.StreamRestore {
2200+ public struct Info {
2201+ string name;
2202+ PulseAudio.ChannelMap channel_map;
2203+ PulseAudio.CVolume volume;
2204+ string device;
2205+ int mute;
2206+ }
2207+
2208+ public delegate void TestCb (PulseAudio.Context context, uint32 version);
2209+ [CCode (cname="pa_ext_stream_restore_test")]
2210+ PulseAudio.Operation? test (PulseAudio.Context context, TestCb? callback = null);
2211+
2212+ public delegate void ReadCb (PulseAudio.Context context, PulseAudio.Extension.StreamRestore.Info[] streams, int eol);
2213+ [CCode (cname="pa_ext_stream_restore_read")]
2214+ PulseAudio.Operation? read (PulseAudio.Context context, ReadCb? callback = null);
2215+}

Subscribers

People subscribed via source and target branches