Merge lp:~charlesk/indicator-sound/volume-warning into lp:indicator-sound/15.10

Proposed by Charles Kerr
Status: Merged
Approved by: Xavi Garcia
Approved revision: 642
Merged at revision: 522
Proposed branch: lp:~charlesk/indicator-sound/volume-warning
Merge into: lp:indicator-sound/15.10
Prerequisite: lp:~xavi-garcia-mena/indicator-sound/action-sync-volume
Diff against target: 3381 lines (+1469/-896)
31 files modified
CMakeLists.txt (+2/-0)
debian/control (+1/-1)
po/POTFILES.in (+4/-0)
src/CMakeLists.txt (+60/-1)
src/Makefile.am.THIS (+0/-39)
src/accounts-service-user.vala (+2/-2)
src/info-notification.vala (+123/-0)
src/main.c (+11/-3)
src/media-player-mpris.vala (+3/-3)
src/media-player-user.vala (+1/-2)
src/mpris2-interfaces.vala (+9/-9)
src/notification.vala (+70/-0)
src/options-gsettings.vala (+87/-0)
src/options.vala (+28/-0)
src/service.vala (+114/-477)
src/volume-control-pulse.vala (+84/-286)
src/volume-control.vala (+18/-14)
src/volume-warning-pulse.vala (+211/-0)
src/volume-warning.vala (+216/-0)
src/warn-notification.vala (+59/-0)
tests/CMakeLists.txt (+10/-1)
tests/integration/indicator-sound-test-base.cpp (+25/-7)
tests/integration/indicator-sound-test-base.h (+2/-0)
tests/integration/test-indicator.cpp (+8/-7)
tests/notifications-mock.h (+1/-1)
tests/notifications-test.cc (+208/-29)
tests/options-mock.vala (+28/-0)
tests/pa-mock.cpp (+19/-0)
tests/volume-control-mock.vala (+19/-13)
tests/volume-control-test.cc (+6/-1)
tests/volume-warning-mock.vala (+40/-0)
To merge this branch: bzr merge lp:~charlesk/indicator-sound/volume-warning
Reviewer Review Type Date Requested Status
Xavi Garcia Approve
PS Jenkins bot (community) continuous-integration Approve
Review via email: mp+281411@code.launchpad.net

Commit message

Be more selective about when to show and dismiss the High Volume Warning Dialog.

Description of the change

Be more selective about when to show and dismiss the High Volume Warning Dialog.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
611. By charles kerr <email address hidden>

remove unused property Notification.visible

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
612. By charles kerr <email address hidden>

fix rounding regression in info-notification's volume percentage

613. By charles kerr <email address hidden>

simplify the Options class' properties

614. By charles kerr <email address hidden>

when showing volume % in notification, clamp to the range of [0%..100%]

615. By charles kerr <email address hidden>

when showing volume notifications, show as a percentage of max-volume

616. By charles kerr <email address hidden>

when max-volume changes, update volume action state & info notification

617. By charles kerr <email address hidden>

fix and re-enable NotificationsTest::ExtendendVolumeNotification

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
618. By charles kerr <email address hidden>

make VolumeControl::active_output easier to mock for tests

619. By charles kerr <email address hidden>

fill out volume-warning-mock's mocking capabilities

620. By charles kerr <email address hidden>

in mock Notify server, support actions required for volume warnings

621. By charles kerr <email address hidden>

add new Notifications test for volume-warning requiring headphones

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
622. By charles kerr <email address hidden>

rename VolumeWarning.high_volume_approved as VolumeWarning.approved

After being moved into a class named "VolumeWarning", the high_volume_
prefix was redunant.

623. By charles kerr <email address hidden>

in Options, make const strings const

624. By charles kerr <email address hidden>

copyediting: whitespace

625. By charles kerr <email address hidden>

in info-warning's icon picking code, fold redundant cases into one.

It looks like these switch statements are intended for future expansion,
so leaving the basic switching logic intact

626. By charles kerr <email address hidden>

update POTFILES.in

627. By charles kerr <email address hidden>

copyediting: yet more whitespace

628. By charles kerr <email address hidden>

simplify some code in volume-warning and volume-warning-pulse

629. By charles kerr <email address hidden>

use predefined VariantTypes instead of reinventing the wheel

630. By charles kerr <email address hidden>

fix references to unowned strings returned by Proplist.gets()

631. By charles kerr <email address hidden>

fix references to unowned strings returned by Environment.get_variable()

632. By charles kerr <email address hidden>

use the 'unowned' keyword when picking from lists of hardcoded strings

633. By charles kerr <email address hidden>

in icon picking code, fold redundant cases together

634. By charles kerr <email address hidden>

re-add notifications-test to tests

635. By charles kerr <email address hidden>

expand volume warning test to include all trigger requirements

636. By charles kerr <email address hidden>

use a custom timer to speed up TriggerWarning's many test combinations

637. By charles kerr <email address hidden>

in notifications-test, make the loop test more flexible

638. By charles kerr <email address hidden>

typo fix: s/Usb/USB/ in ui strings

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Xavi Garcia (xavi-garcia-mena) wrote :

It looks better than the initial approach.

What I see, though, is that you moved some of the new strings for the OSD notifications to different files...
I don´t know if that´s going to be a problem with the translations if we don´t land this today.
I also see you changed a couple of strings (from Usb to USB).

I´m not approving yet, as it still has those weird conflicts and it´s a bit hard to read, but overall it looks good to me :)

review: Needs Information
639. By Charles Kerr

revert previous commit to preserve translation strings (eg, s/USB/Usb/)

640. By Charles Kerr

sync with trunk

Revision history for this message
Charles Kerr (charlesk) wrote :

> What I see, though, is that you moved some of the new strings for the OSD notifications to different files...

Moving the strings from one file to another should be alright as long as the new files are included in POTFILES.in, which they are.

> I also see you changed a couple of strings (from Usb to USB).

Agreed, that's bad timing.

Reverted r639.

> I´m not approving yet, as it still has those weird conflicts and it´s a bit hard to read

Working on the conflicts now. I hope you meant that's why it's hard to read ;)

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
641. By Charles Kerr

sync with trunk.15.10

642. By Charles Kerr

re-enable notifications-test

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Xavi Garcia (xavi-garcia-mena) wrote :

Yeah, it was hard to read due to the conflicts.

Approving, it looks good to me. Thanks!

review: Approve
643. By Charles Kerr

in volume-warning-pulse, get/set the sink input's volume rather than the sink's volume, just as volume-control-pulse does

644. By Charles Kerr

remove newly-dead variable VolumeWarningPulse._update_sink_timer

645. By Charles Kerr

change a tracer GLib.message() call to GLib.debug()

646. By Charles Kerr

in the integration tests, clamp the random volume selection to a range that won't cause the loud-volume-warning code to interfere with the test

647. By Charles Kerr

disable integration test PhoneChangeRoleVolume, amd64 failure seems unrelated to these changes & needs fixing in separate MP

648. By Charles Kerr

in tests' PulseAudio mock, add pa_context_get_sink_input_info_list() implementation

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-11-17 21:00:12 +0000
3+++ CMakeLists.txt 2016-01-13 20:35:42 +0000
4@@ -24,12 +24,14 @@
5 set(SOURCE_BINARY_DIR "${CMAKE_BINARY_DIR}/src")
6
7 set(PULSE_AUDIO_REQUIRED_VERSION 0.9.19)
8+set(GLIB_2_0_REQUIRED_VERSION 2.32)
9 set(GIO_2_0_REQUIRED_VERSION 2.25.13)
10 set(URL_DISPATCHER_1_REQUIRED_VERSION 1)
11
12 pkg_check_modules(
13 PULSEAUDIO REQUIRED
14 libpulse-mainloop-glib>=${PULSE_AUDIO_REQUIRED_VERSION}
15+ glib-2.0>=${GLIB_2_0_REQUIRED_VERSION}
16 gio-unix-2.0>=${GIO_2_0_REQUIRED_VERSION}
17 url-dispatcher-1>=${URL_DISPATCHER_1_REQUIRED_VERSION}
18 )
19
20=== modified file 'debian/control'
21--- debian/control 2015-11-11 11:48:43 +0000
22+++ debian/control 2016-01-13 20:35:42 +0000
23@@ -18,7 +18,7 @@
24 libaccountsservice-dev,
25 libdbustest1-dev (>= 15.04.0),
26 libgirepository1.0-dev,
27- libglib2.0-dev (>= 2.22.3),
28+ libglib2.0-dev (>= 2.32.0),
29 libgtest-dev,
30 libqtdbusmock1-dev (>= 0.3),
31 libqtdbustest1-dev,
32
33=== modified file 'po/POTFILES.in'
34--- po/POTFILES.in 2013-09-11 09:47:53 +0000
35+++ po/POTFILES.in 2016-01-13 20:35:42 +0000
36@@ -1,3 +1,7 @@
37 [encoding: UTF-8]
38+src/info-notification.vala
39+src/options-gsettings.vala
40 src/service.vala
41 src/sound-menu.vala
42+src/warn-notification.vala
43+
44
45=== modified file 'src/CMakeLists.txt'
46--- src/CMakeLists.txt 2015-12-23 13:35:46 +0000
47+++ src/CMakeLists.txt 2016-01-13 20:35:42 +0000
48@@ -32,6 +32,7 @@
49 OPTIONS
50 --ccode
51 --thread
52+ --target-glib=${GLIB_2_0_REQUIRED_VERSION}
53 --vapidir=${CMAKE_SOURCE_DIR}/vapi/
54 --vapidir=.
55 --pkg=url-dispatcher
56@@ -39,23 +40,79 @@
57 )
58
59 vala_add(indicator-sound-service
60+ notification.vala
61+)
62+vala_add(indicator-sound-service
63+ info-notification.vala
64+ DEPENDS
65+ notification
66+ volume-control
67+ options
68+)
69+vala_add(indicator-sound-service
70+ warn-notification.vala
71+ DEPENDS
72+ notification
73+)
74+vala_add(indicator-sound-service
75 service.vala
76 DEPENDS
77 sound-menu
78 volume-control
79 volume-control-pulse
80+ notification
81+ info-notification
82+ volume-warning
83+ options
84+ options-gsettings
85 media-player
86 media-player-list
87 mpris2-interfaces
88 accounts-service-user
89 )
90 vala_add(indicator-sound-service
91+ options.vala
92+ DEPENDS
93+ volume-control
94+ volume-control-pulse
95+)
96+vala_add(indicator-sound-service
97+ options-gsettings.vala
98+ DEPENDS
99+ options
100+ volume-control-pulse
101+ volume-control
102+)
103+vala_add(indicator-sound-service
104 volume-control.vala
105+ DEPENDS
106+ options
107+ volume-control-pulse
108 )
109 vala_add(indicator-sound-service
110 volume-control-pulse.vala
111 DEPENDS
112- volume-control
113+ options
114+ volume-control
115+)
116+vala_add(indicator-sound-service
117+ volume-warning.vala
118+ DEPENDS
119+ options
120+ volume-control-pulse
121+ volume-control
122+ warn-notification
123+ notification
124+)
125+vala_add(indicator-sound-service
126+ volume-warning-pulse.vala
127+ DEPENDS
128+ volume-warning
129+ options
130+ volume-control-pulse
131+ volume-control
132+ warn-notification
133+ notification
134 )
135 vala_add(indicator-sound-service
136 media-player.vala
137@@ -104,6 +161,8 @@
138 DEPENDS
139 media-player
140 volume-control
141+ options
142+ volume-control-pulse
143 )
144 vala_add(indicator-sound-service
145 accounts-service-user.vala
146
147=== removed file 'src/Makefile.am.THIS'
148--- src/Makefile.am.THIS 2013-08-26 14:51:33 +0000
149+++ src/Makefile.am.THIS 1970-01-01 00:00:00 +0000
150@@ -1,39 +0,0 @@
151-pkglibexec_PROGRAMS = indicator-sound-service
152-
153-indicator_sound_service_SOURCES = \
154- service.vala \
155- main.vala \
156- volume-control.vala \
157- media-player.vala \
158- media-player-list.vala \
159- mpris2-interfaces.vala \
160- freedesktop-interfaces.vala \
161- sound-menu.vala \
162- bus-watch-namespace.c \
163- bus-watch-namespace.h
164-
165-indicator_sound_service_VALAFLAGS = \
166- --ccode \
167- --vapidir=$(top_srcdir)/vapi/ \
168- --vapidir=./ \
169- --thread \
170- --pkg config \
171- --pkg gio-2.0 \
172- --pkg gio-unix-2.0 \
173- --pkg libxml-2.0 \
174- --pkg libpulse \
175- --pkg libpulse-mainloop-glib \
176- --pkg bus-watcher \
177- --target-glib=2.36
178-
179-# -w to disable warnings for vala-generated code
180-indicator_sound_service_CFLAGS = $(PULSEAUDIO_CFLAGS) \
181- $(SOUNDSERVICE_CFLAGS) \
182- $(GCONF_CFLAGS) \
183- $(COVERAGE_CFLAGS) \
184- -DLIBEXECDIR=\"$(libexecdir)\" \
185- -w \
186- -DGETTEXT_PACKAGE=\"$(GETTEXT_PACKAGE)\"
187-
188-indicator_sound_service_LDADD = $(PULSEAUDIO_LIBS) $(SOUNDSERVICE_LIBS) $(GCONF_LIBS)
189-indicator_sound_service_LDFLAGS = $(COVERAGE_LDFLAGS)
190
191=== modified file 'src/accounts-service-user.vala'
192--- src/accounts-service-user.vala 2014-12-09 15:55:37 +0000
193+++ src/accounts-service-user.vala 2016-01-13 20:35:42 +0000
194@@ -185,7 +185,7 @@
195 this.privacyproxy = Bus.get_proxy.end (res);
196
197 (this.privacyproxy as DBusProxy).g_properties_changed.connect((proxy, changed, invalid) => {
198- var welcomeval = changed.lookup_value("MessagesWelcomeScreen", new VariantType("b"));
199+ var welcomeval = changed.lookup_value("MessagesWelcomeScreen", VariantType.BOOLEAN);
200 if (welcomeval != null) {
201 debug("Messages on welcome screen changed");
202 this.showDataOnGreeter = welcomeval.get_boolean();
203@@ -204,7 +204,7 @@
204 this.syssoundproxy = Bus.get_proxy.end (res);
205
206 (this.syssoundproxy as DBusProxy).g_properties_changed.connect((proxy, changed, invalid) => {
207- var silentvar = changed.lookup_value("SilentMode", new VariantType("b"));
208+ var silentvar = changed.lookup_value("SilentMode", VariantType.BOOLEAN);
209 if (silentvar != null) {
210 debug("Silent Mode changed");
211 this._silentMode = silentvar.get_boolean();
212
213=== added file 'src/info-notification.vala'
214--- src/info-notification.vala 1970-01-01 00:00:00 +0000
215+++ src/info-notification.vala 2016-01-13 20:35:42 +0000
216@@ -0,0 +1,123 @@
217+/*
218+ * Copyright 2015 Canonical Ltd.
219+ *
220+ * This program is free software; you can redistribute it and/or modify
221+ * it under the terms of the GNU General Public License as published by
222+ * the Free Software Foundation; version 3.
223+ *
224+ * This program is distributed in the hope that it will be useful,
225+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
226+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
227+ * GNU General Public License for more details.
228+ *
229+ * You should have received a copy of the GNU General Public License
230+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
231+ *
232+ * Authors:
233+ * Charles Kerr <charles.kerr@canonical.com>
234+ */
235+
236+using Notify;
237+
238+public class IndicatorSound.InfoNotification: Notification
239+{
240+ protected override Notify.Notification create_notification () {
241+ return new Notify.Notification (_("Volume"), "", "audio-volume-muted");
242+ }
243+
244+ public void show (VolumeControl.ActiveOutput active_output,
245+ double volume,
246+ bool is_high_volume) {
247+ if (!notify_server_supports ("x-canonical-private-synchronous"))
248+ return;
249+
250+ /* Determine Label */
251+ unowned string volume_label = get_notification_label (active_output);
252+
253+ /* Choose an icon */
254+ unowned string icon = get_volume_notification_icon (active_output, volume, is_high_volume);
255+
256+ /* Reset the notification */
257+ var n = _notification;
258+ n.update (_("Volume"), volume_label, icon);
259+ n.clear_hints();
260+ n.set_hint ("x-canonical-non-shaped-icon", "true");
261+ n.set_hint ("x-canonical-private-synchronous", "true");
262+ n.set_hint ("x-canonical-value-bar-tint", is_high_volume ? "true" : "false");
263+ n.set_hint ("value", ((int32)((volume * 100.0) + 0.5)).clamp(0, 100));
264+ show_notification ();
265+ }
266+
267+ private static unowned string get_notification_label (VolumeControl.ActiveOutput active_output) {
268+
269+ switch (active_output) {
270+ case VolumeControl.ActiveOutput.SPEAKERS:
271+ return _("Speakers");
272+ case VolumeControl.ActiveOutput.HEADPHONES:
273+ return _("Headphones");
274+ case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
275+ return _("Bluetooth headphones");
276+ case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER:
277+ return _("Bluetooth speaker");
278+ case VolumeControl.ActiveOutput.USB_SPEAKER:
279+ return _("Usb speaker");
280+ case VolumeControl.ActiveOutput.USB_HEADPHONES:
281+ return _("Usb headphones");
282+ case VolumeControl.ActiveOutput.HDMI_SPEAKER:
283+ return _("HDMI speaker");
284+ case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
285+ return _("HDMI headphones");
286+ default:
287+ return "";
288+ }
289+ }
290+
291+ private static unowned string get_volume_notification_icon (VolumeControl.ActiveOutput active_output,
292+ double volume,
293+ bool is_high_volume) {
294+
295+ if (!is_high_volume)
296+ return get_volume_icon (active_output, volume);
297+
298+ switch (active_output) {
299+ case VolumeControl.ActiveOutput.SPEAKERS:
300+ case VolumeControl.ActiveOutput.HEADPHONES:
301+ case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
302+ case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER:
303+ case VolumeControl.ActiveOutput.USB_SPEAKER:
304+ case VolumeControl.ActiveOutput.USB_HEADPHONES:
305+ case VolumeControl.ActiveOutput.HDMI_SPEAKER:
306+ case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
307+ return "audio-volume-high";
308+
309+ default:
310+ return "";
311+ }
312+ }
313+
314+ private static unowned string get_volume_icon (VolumeControl.ActiveOutput active_output,
315+ double volume)
316+ {
317+ switch (active_output) {
318+ case VolumeControl.ActiveOutput.SPEAKERS:
319+ case VolumeControl.ActiveOutput.HEADPHONES:
320+ case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
321+ case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER:
322+ case VolumeControl.ActiveOutput.USB_SPEAKER:
323+ case VolumeControl.ActiveOutput.USB_HEADPHONES:
324+ case VolumeControl.ActiveOutput.HDMI_SPEAKER:
325+ case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
326+ if (volume <= 0.0)
327+ return "audio-volume-muted";
328+ if (volume <= 0.3)
329+ return "audio-volume-low";
330+ if (volume <= 0.7)
331+ return "audio-volume-medium";
332+ return "audio-volume-high";
333+
334+ default:
335+ return "";
336+ }
337+ }
338+}
339+
340
341=== modified file 'src/main.c'
342--- src/main.c 2015-03-10 19:16:26 +0000
343+++ src/main.c 2016-01-13 20:35:42 +0000
344@@ -22,6 +22,7 @@
345 #include "config.h"
346
347 static IndicatorSoundService * service = NULL;
348+static pa_glib_mainloop * pgloop = NULL;
349
350 static gboolean
351 sigterm_handler (gpointer data)
352@@ -46,8 +47,10 @@
353 gpointer user_data)
354 {
355 MediaPlayerList * playerlist = NULL;
356+ IndicatorSoundOptions * options = NULL;
357 VolumeControlPulse * volume = NULL;
358 AccountsServiceUser * accounts = NULL;
359+ VolumeWarning * warning = NULL;
360
361
362 if (g_strcmp0("lightdm", g_get_user_name()) == 0) {
363@@ -57,20 +60,24 @@
364 accounts = accounts_service_user_new();
365 }
366
367- volume = volume_control_pulse_new();
368+ pgloop = pa_glib_mainloop_new(NULL);
369+ options = indicator_sound_options_gsettings_new();
370+ volume = volume_control_pulse_new(options, pgloop);
371+ warning = volume_warning_pulse_new(options, pgloop);
372
373- service = indicator_sound_service_new (playerlist, volume, accounts);
374+ service = indicator_sound_service_new (playerlist, volume, accounts, options, warning);
375
376 g_clear_object(&playerlist);
377+ g_clear_object(&options);
378 g_clear_object(&volume);
379 g_clear_object(&accounts);
380+ g_clear_object(&warning);
381 }
382
383 int
384 main (int argc, char ** argv)
385 {
386 GMainLoop * loop = NULL;
387-
388 bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
389 setlocale (LC_ALL, "");
390 bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
391@@ -95,6 +102,7 @@
392 g_main_loop_run(loop);
393
394 g_clear_object(&service);
395+ g_clear_pointer(&pgloop, pa_glib_mainloop_free);
396
397 notify_uninit();
398
399
400=== modified file 'src/media-player-mpris.vala'
401--- src/media-player-mpris.vala 2015-10-21 13:16:58 +0000
402+++ src/media-player-mpris.vala 2016-01-13 20:35:42 +0000
403@@ -226,7 +226,7 @@
404 if (this.play_when_attached) {
405 /* wait a little before calling PlayPause, some players need some time to
406 set themselves up */
407- Timeout.add (1000, () => { proxy.PlayPause.begin (); return false; } );
408+ Timeout.add (1000, () => { proxy.PlayPause.begin (); return Source.REMOVE; } );
409 this.play_when_attached = false;
410 }
411 }
412@@ -269,7 +269,7 @@
413 return;
414 }
415
416- Timeout.add (500, () => { this.fetch_playlists (); return false; } );
417+ Timeout.add (500, () => { this.fetch_playlists (); return Source.REMOVE; } );
418 }
419
420 /* some players (e.g. Spotify) don't follow the spec closely and pass single strings in metadata fields
421@@ -295,7 +295,7 @@
422 this.playbackstatus_changed ();
423 }
424
425- var metadata = changed_properties.lookup_value ("Metadata", new VariantType ("a{sv}"));
426+ var metadata = changed_properties.lookup_value ("Metadata", VariantType.VARDICT);
427 if (metadata != null)
428 this.update_current_track (metadata);
429 }
430
431=== modified file 'src/media-player-user.vala'
432--- src/media-player-user.vala 2014-03-04 21:40:28 +0000
433+++ src/media-player-user.vala 2016-01-13 20:35:42 +0000
434@@ -75,8 +75,7 @@
435
436 properties_queued.remove_all();
437
438- /* Remove source */
439- return false;
440+ return Source.REMOVE;
441 }
442
443 /* Turns the DBus names into the object properties */
444
445=== modified file 'src/mpris2-interfaces.vala'
446--- src/mpris2-interfaces.vala 2015-09-15 12:16:10 +0000
447+++ src/mpris2-interfaces.vala 2016-01-13 20:35:42 +0000
448@@ -1,18 +1,18 @@
449 /*
450-Copyright 2010 Canonical Ltd.
451+Copyright 2010-2015 Canonical Ltd.
452 Authors:
453 Conor Curran <conor.curran@canonical.com>
454
455-This program is free software: you can redistribute it and/or modify it
456-under the terms of the GNU General Public License version 3, as published
457+This program is free software: you can redistribute it and/or modify it
458+under the terms of the GNU General Public License version 3, as published
459 by the Free Software Foundation.
460
461-This program is distributed in the hope that it will be useful, but
462-WITHOUT ANY WARRANTY; without even the implied warranties of
463-MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
464+This program is distributed in the hope that it will be useful, but
465+WITHOUT ANY WARRANTY; without even the implied warranties of
466+MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
467 PURPOSE. See the GNU General Public License for more details.
468
469-You should have received a copy of the GNU General Public License along
470+You should have received a copy of the GNU General Public License along
471 with this program. If not, see <http://www.gnu.org/licenses/>.
472 */
473
474@@ -36,8 +36,8 @@
475 public interface MprisPlayer : Object {
476 // properties
477 public abstract HashTable<string, Variant?> Metadata{owned get; set;}
478- public abstract int32 Position{owned get; set;}
479- public abstract string? PlaybackStatus{owned get; set;}
480+ public abstract int64 Position{owned get; set;}
481+ public abstract string? PlaybackStatus{owned get; set;}
482 public abstract bool CanPlay{owned get; set;}
483 public abstract bool CanGoNext{owned get; set;}
484 public abstract bool CanGoPrevious{owned get; set;}
485
486=== added file 'src/notification.vala'
487--- src/notification.vala 1970-01-01 00:00:00 +0000
488+++ src/notification.vala 2016-01-13 20:35:42 +0000
489@@ -0,0 +1,70 @@
490+/*
491+ * Copyright 2015 Canonical Ltd.
492+ *
493+ * This program is free software; you can redistribute it and/or modify
494+ * it under the terms of the GNU General Public License as published by
495+ * the Free Software Foundation; version 3.
496+ *
497+ * This program is distributed in the hope that it will be useful,
498+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
499+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
500+ * GNU General Public License for more details.
501+ *
502+ * You should have received a copy of the GNU General Public License
503+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
504+ *
505+ * Authors:
506+ * Charles Kerr <charles.kerr@canonical.com>
507+ */
508+
509+public abstract class IndicatorSound.Notification: Object
510+{
511+ public Notification () {
512+ BusWatcher.watch_namespace (
513+ GLib.BusType.SESSION,
514+ "org.freedesktop.Notifications",
515+ () => { debug ("Notifications name appeared"); },
516+ () => { debug ("Notifications name vanshed"); _server_caps = null; });
517+
518+ _notification = create_notification ();
519+ }
520+
521+ public void close () {
522+ var n = _notification;
523+
524+ return_if_fail (n != null);
525+
526+ if (n.id != 0) {
527+ try {
528+ n.close ();
529+ } catch (GLib.Error e) {
530+ GLib.warning ("Unable to close notification: %s", e.message);
531+ }
532+ }
533+ }
534+
535+ ~Notification () {
536+ close ();
537+ }
538+
539+ protected abstract Notify.Notification create_notification ();
540+
541+ protected void show_notification () {
542+ try {
543+ _notification.show ();
544+ } catch (GLib.Error e) {
545+ GLib.warning ("Unable to show notification: %s", e.message);
546+ }
547+ }
548+
549+ protected bool notify_server_supports (string cap) {
550+ if (_server_caps == null)
551+ _server_caps = Notify.get_server_caps ();
552+
553+ return _server_caps.find_custom (cap, strcmp) != null;
554+ }
555+
556+ protected Notify.Notification _notification = null;
557+
558+ private static List<string> _server_caps = null;
559+}
560
561=== added file 'src/options-gsettings.vala'
562--- src/options-gsettings.vala 1970-01-01 00:00:00 +0000
563+++ src/options-gsettings.vala 2016-01-13 20:35:42 +0000
564@@ -0,0 +1,87 @@
565+/*
566+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
567+ * Copyright 2015 Canonical Ltd.
568+ *
569+ * This program is free software; you can redistribute it and/or modify
570+ * it under the terms of the GNU General Public License as published by
571+ * the Free Software Foundation; version 3.
572+ *
573+ * This program is distributed in the hope that it will be useful,
574+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
575+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
576+ * GNU General Public License for more details.
577+ *
578+ * You should have received a copy of the GNU General Public License
579+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
580+ *
581+ * Authors:
582+ * Charles Kerr <charles.kerr@canonical.com>
583+ */
584+
585+using PulseAudio;
586+
587+public class IndicatorSound.OptionsGSettings : Options
588+{
589+ public OptionsGSettings() {
590+ init_max_volume();
591+ init_loud_volume();
592+ }
593+
594+ ~OptionsGSettings() {
595+ }
596+
597+ private Settings _settings = new Settings ("com.canonical.indicator.sound");
598+ private Settings _shared_settings = new Settings ("com.ubuntu.sound");
599+
600+ /** MAX VOLUME PROPERTY **/
601+
602+ private static const string AMP_dB_KEY = "amplified-volume-decibels";
603+ private static const string NORMAL_dB_KEY = "normal-volume-decibels";
604+ private static const string ALLOW_AMP_KEY = "allow-amplified-volume";
605+
606+ private void init_max_volume() {
607+ _settings.changed[NORMAL_dB_KEY].connect(() => update_max_volume());
608+ _settings.changed[AMP_dB_KEY].connect(() => update_max_volume());
609+ _shared_settings.changed[ALLOW_AMP_KEY].connect(() => update_max_volume());
610+ update_max_volume();
611+ }
612+ private void update_max_volume () {
613+ set_max_volume_(calculate_max_volume());
614+ }
615+ protected void set_max_volume_ (double vol) {
616+ if (max_volume != vol) {
617+ debug("changing max_volume from %f to %f", this.max_volume, vol);
618+ max_volume = vol;
619+ }
620+ }
621+ private double calculate_max_volume () {
622+ unowned string decibel_key = _shared_settings.get_boolean(ALLOW_AMP_KEY)
623+ ? AMP_dB_KEY
624+ : NORMAL_dB_KEY;
625+ var volume_dB = _settings.get_double(decibel_key);
626+ var volume_sw = PulseAudio.Volume.sw_from_dB (volume_dB);
627+ return VolumeControlPulse.volume_to_double (volume_sw);
628+ }
629+
630+
631+ /** LOUD VOLUME **/
632+
633+ private static const string LOUD_ENABLED_KEY = "warning-volume-enabled";
634+ private static const string LOUD_DECIBEL_KEY = "warning-volume-decibels";
635+
636+ private void init_loud_volume() {
637+ _settings.changed[LOUD_ENABLED_KEY].connect(() => update_loud_volume());
638+ _settings.changed[LOUD_DECIBEL_KEY].connect(() => update_loud_volume());
639+ update_loud_volume();
640+ }
641+ private void update_loud_volume() {
642+
643+ var vol = PulseAudio.Volume.sw_from_dB (_settings.get_double (LOUD_DECIBEL_KEY));
644+ if (loud_volume != vol)
645+ loud_volume = vol;
646+
647+ var enabled = _settings.get_boolean(LOUD_ENABLED_KEY);
648+ if (loud_warning_enabled != enabled)
649+ loud_warning_enabled = enabled;
650+ }
651+}
652
653=== added file 'src/options.vala'
654--- src/options.vala 1970-01-01 00:00:00 +0000
655+++ src/options.vala 2016-01-13 20:35:42 +0000
656@@ -0,0 +1,28 @@
657+/*
658+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
659+ * Copyright 2015 Canonical Ltd.
660+ *
661+ * This program is free software; you can redistribute it and/or modify
662+ * it under the terms of the GNU General Public License as published by
663+ * the Free Software Foundation; version 3.
664+ *
665+ * This program is distributed in the hope that it will be useful,
666+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
667+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
668+ * GNU General Public License for more details.
669+ *
670+ * You should have received a copy of the GNU General Public License
671+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
672+ *
673+ * Authors:
674+ * Charles Kerr <charles.kerr@canonical.com>
675+ */
676+
677+public abstract class IndicatorSound.Options : Object
678+{
679+ public double max_volume { get; protected set; default = 1.0; }
680+
681+ public uint loud_volume { get; protected set; default = PulseAudio.Volume.sw_from_dB(8); }
682+
683+ public bool loud_warning_enabled { get; protected set; default = true; }
684+}
685
686=== modified file 'src/service.vala'
687--- src/service.vala 2016-01-05 15:08:24 +0000
688+++ src/service.vala 2016-01-13 20:35:42 +0000
689@@ -20,35 +20,25 @@
690 public class IndicatorSound.Service: Object {
691 DBusConnection bus;
692
693- /**
694- * A copy of volume_control.volume made before just warn_notification
695- * is shown. Since the volume is clamped during the warning, we cache
696- * the previous volume to use iff the user hits "OK".
697- */
698- VolumeControl.Volume _pre_warn_volume = null;
699+ public Service (MediaPlayerList playerlist, VolumeControl volume, AccountsServiceUser? accounts, Options options, VolumeWarning volume_warning) {
700
701- public Service (MediaPlayerList playerlist, VolumeControl volume, AccountsServiceUser? accounts) {
702 try {
703 bus = Bus.get_sync(GLib.BusType.SESSION);
704 } catch (GLib.Error e) {
705 error("Unable to get DBus session bus: %s", e.message);
706 }
707
708- info_notification = new Notify.Notification(_("Volume"), "", "audio-volume-muted");
709+ _options = options;
710+ _options.notify["max-volume"].connect(() => {
711+ update_volume_action_state();
712+ this.update_notification();
713+ });
714
715- warn_notification = new Notify.Notification(_("Volume"), _("High volume can damage your hearing."), "audio-volume-high");
716- warn_notification.set_hint ("x-canonical-non-shaped-icon", "true");
717- warn_notification.set_hint ("x-canonical-snap-decisions", "true");
718- warn_notification.set_hint ("x-canonical-private-affirmative-tint", "true");
719- warn_notification.closed.connect((n) => {
720- n.clear_actions();
721- waiting_user_approve_warn=false;
722- increment_volume_sync_action();
723+ _volume_warning = volume_warning;
724+ _volume_warning.notify["active"].connect(() => {
725+ this.increment_volume_sync_action();
726+ this.update_notification();
727 });
728- BusWatcher.watch_namespace (GLib.BusType.SESSION,
729- "org.freedesktop.Notifications",
730- () => { debug("Notifications name appeared"); },
731- () => { debug("Notifications name vanshed"); notify_server_caps_checked = false; });
732
733 this.settings = new Settings ("com.canonical.indicator.sound");
734
735@@ -56,8 +46,26 @@
736 this.notify["visible"].connect ( () => this.update_root_icon () );
737
738 this.volume_control = volume;
739- this.volume_control.active_output_changed.connect (this.update_root_icon);
740- this.volume_control.active_output_changed.connect (this.update_notification);
741+ this.volume_control.active_output_changed.connect(() => {
742+ bool headphones;
743+ switch(volume_control.active_output()) {
744+ case VolumeControl.ActiveOutput.HEADPHONES:
745+ case VolumeControl.ActiveOutput.USB_HEADPHONES:
746+ case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
747+ case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
748+ headphones = true;
749+ break;
750+
751+ default:
752+ headphones = false;
753+ break;
754+ }
755+ message("setting _volume_warning.headphones_active to %d", (int)headphones);
756+ _volume_warning.headphones_active = headphones;
757+
758+ update_root_icon();
759+ update_notification();
760+ });
761
762 this.accounts_service = accounts;
763 /* If we're on the greeter, don't export */
764@@ -94,7 +102,7 @@
765 });
766
767 this.menus.@foreach ( (profile, menu) => {
768- this.volume_control.bind_property ("high-volume", menu, "show-high-volume-warning", BindingFlags.SYNC_CREATE);
769+ _volume_warning.bind_property ("high-volume", menu, "show-high-volume-warning", BindingFlags.SYNC_CREATE);
770 });
771
772 this.menus.@foreach ( (profile, menu) => {
773@@ -112,7 +120,7 @@
774 block_info_notifications = state.get_boolean();
775 if (block_info_notifications) {
776 debug("Indicator is shown");
777- close_notification(info_notification);
778+ _info_notification.close();
779 } else {
780 debug("Indicator is hidden");
781 }
782@@ -128,33 +136,11 @@
783 this.menus.@foreach ( (profile, menu) => menu.export (bus, @"/com/canonical/indicator/sound/$profile"));
784 }
785
786- private void close_notification(Notify.Notification? n) {
787- return_if_fail (n != null);
788- if (n.id != 0) {
789- try {
790- n.close();
791- } catch (GLib.Error e) {
792- warning("Unable to close notification: %s", e.message);
793- }
794- }
795- }
796-
797- private void show_notification(Notify.Notification? n) {
798- return_if_fail (n != null);
799- try {
800- n.show ();
801- } catch (GLib.Error e) {
802- warning ("Unable to show notification: %s", e.message);
803- }
804- }
805-
806 ~Service() {
807 debug("Destroying Service Object");
808
809 clear_acts_player();
810
811- stop_clamp_to_high_timeout();
812-
813 if (this.player_action_update_id > 0) {
814 Source.remove (this.player_action_update_id);
815 this.player_action_update_id = 0;
816@@ -203,19 +189,29 @@
817 bool syncing_preferred_players = false;
818 AccountsServiceUser? accounts_service = null;
819 bool export_to_accounts_service = false;
820- private Notify.Notification info_notification;
821- private Notify.Notification warn_notification;
822+ private Options _options;
823+ private VolumeWarning _volume_warning;
824+ private IndicatorSound.InfoNotification _info_notification = new IndicatorSound.InfoNotification();
825
826 const double volume_step_percentage = 0.06;
827
828 private void activate_scroll_action (SimpleAction action, Variant? param) {
829- int delta = param.get_int32(); /* positive for up, negative for down */
830- double v = volume_control.volume.volume + volume_step_percentage * delta;
831- volume_control.set_volume_clamp (v, VolumeControl.VolumeReasons.USER_KEYPRESS);
832+ int direction = param.get_int32(); // positive for up, negative for down
833+ message("scroll: %d", direction);
834+
835+ if (_volume_warning.active) {
836+ _volume_warning.user_keypress(direction>0
837+ ? VolumeWarning.Key.VOLUME_UP
838+ : VolumeWarning.Key.VOLUME_DOWN);
839+ } else {
840+ double delta = volume_step_percentage * direction;
841+ double v = volume_control.volume.volume + delta;
842+ volume_control.set_volume_clamp (v, VolumeControl.VolumeReasons.USER_KEYPRESS);
843+ }
844 }
845
846 void activate_desktop_settings (SimpleAction action, Variant? param) {
847- var env = Environment.get_variable ("DESKTOP_SESSION");
848+ unowned string env = Environment.get_variable ("DESKTOP_SESSION");
849 string cmd;
850
851 if (Environment.get_variable ("MIR_SOCKET") != null)
852@@ -256,7 +252,7 @@
853
854 void update_root_icon () {
855 double volume = this.volume_control.volume.volume;
856- string icon = get_volume_root_icon (volume, this.volume_control.mute, volume_control.active_output);
857+ unowned string icon = get_volume_root_icon (volume, this.volume_control.mute, volume_control.active_output());
858
859 string accessible_name;
860 if (this.volume_control.mute) {
861@@ -270,7 +266,7 @@
862 }
863
864 var root_action = actions.lookup_action ("root") as SimpleAction;
865- var builder = new VariantBuilder (new VariantType ("a{sv}"));
866+ var builder = new VariantBuilder (VariantType.VARDICT);
867 builder.add ("{sv}", "title", new Variant.string (_("Sound")));
868 builder.add ("{sv}", "accessible-desc", new Variant.string (accessible_name));
869 builder.add ("{sv}", "icon", serialize_themed_icon (icon));
870@@ -278,397 +274,57 @@
871 root_action.set_state (builder.end());
872 }
873
874- private bool notify_server_caps_checked = false;
875- private bool notify_server_supports_actions = false;
876- private bool notify_server_supports_sync = false;
877 private bool block_info_notifications = false;
878- private bool waiting_user_approve_warn = false;
879-
880- private string get_volume_icon (double volume, VolumeControl.ActiveOutput active_output)
881- {
882- string icon = "";
883- switch (active_output)
884- {
885- case VolumeControl.ActiveOutput.SPEAKERS:
886- if (volume <= 0.0)
887- icon = "audio-volume-muted";
888- else if (volume <= 0.3)
889- icon = "audio-volume-low";
890- else if (volume <= 0.7)
891- icon = "audio-volume-medium";
892- else
893- icon = "audio-volume-high";
894- break;
895- case VolumeControl.ActiveOutput.HEADPHONES:
896- if (volume <= 0.0)
897- icon = "audio-volume-muted";
898- else if (volume <= 0.3)
899- icon = "audio-volume-low";
900- else if (volume <= 0.7)
901- icon = "audio-volume-medium";
902- else
903- icon = "audio-volume-high";
904- break;
905- case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
906- if (volume <= 0.0)
907- icon = "audio-volume-muted";
908- else if (volume <= 0.3)
909- icon = "audio-volume-low";
910- else if (volume <= 0.7)
911- icon = "audio-volume-medium";
912- else
913- icon = "audio-volume-high";
914- break;
915- case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER:
916- if (volume <= 0.0)
917- icon = "audio-volume-muted";
918- else if (volume <= 0.3)
919- icon = "audio-volume-low";
920- else if (volume <= 0.7)
921- icon = "audio-volume-medium";
922- else
923- icon = "audio-volume-high";
924- break;
925- case VolumeControl.ActiveOutput.USB_SPEAKER:
926- if (volume <= 0.0)
927- icon = "audio-volume-muted";
928- else if (volume <= 0.3)
929- icon = "audio-volume-low";
930- else if (volume <= 0.7)
931- icon = "audio-volume-medium";
932- else
933- icon = "audio-volume-high";
934- break;
935- case VolumeControl.ActiveOutput.USB_HEADPHONES:
936- if (volume <= 0.0)
937- icon = "audio-volume-muted";
938- else if (volume <= 0.3)
939- icon = "audio-volume-low";
940- else if (volume <= 0.7)
941- icon = "audio-volume-medium";
942- else
943- icon = "audio-volume-high";
944- break;
945- case VolumeControl.ActiveOutput.HDMI_SPEAKER:
946- if (volume <= 0.0)
947- icon = "audio-volume-muted";
948- else if (volume <= 0.3)
949- icon = "audio-volume-low";
950- else if (volume <= 0.7)
951- icon = "audio-volume-medium";
952- else
953- icon = "audio-volume-high";
954- break;
955- case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
956- if (volume <= 0.0)
957- icon = "audio-volume-muted";
958- else if (volume <= 0.3)
959- icon = "audio-volume-low";
960- else if (volume <= 0.7)
961- icon = "audio-volume-medium";
962- else
963- icon = "audio-volume-high";
964- break;
965- }
966- return icon;
967- }
968-
969- private string get_volume_root_icon_by_volume (double volume, VolumeControl.ActiveOutput active_output)
970- {
971- string icon = "";
972- switch (active_output)
973- {
974- case VolumeControl.ActiveOutput.SPEAKERS:
975- if (volume <= 0.0)
976- icon = "audio-volume-muted-panel";
977- else if (volume <= 0.3)
978- icon = "audio-volume-low-panel";
979- else if (volume <= 0.7)
980- icon = "audio-volume-medium-panel";
981- else
982- icon = "audio-volume-high-panel";
983- break;
984- case VolumeControl.ActiveOutput.HEADPHONES:
985- if (volume <= 0.0)
986- icon = "audio-volume-muted-panel";
987- else if (volume <= 0.3)
988- icon = "audio-volume-low-panel";
989- else if (volume <= 0.7)
990- icon = "audio-volume-medium-panel";
991- else
992- icon = "audio-volume-high-panel";
993- break;
994- case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
995- if (volume <= 0.0)
996- icon = "audio-volume-muted-panel";
997- else if (volume <= 0.3)
998- icon = "audio-volume-low-panel";
999- else if (volume <= 0.7)
1000- icon = "audio-volume-medium-panel";
1001- else
1002- icon = "audio-volume-high-panel";
1003- break;
1004- case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER:
1005- if (volume <= 0.0)
1006- icon = "audio-volume-muted-panel";
1007- else if (volume <= 0.3)
1008- icon = "audio-volume-low-panel";
1009- else if (volume <= 0.7)
1010- icon = "audio-volume-medium-panel";
1011- else
1012- icon = "audio-volume-high-panel";
1013- break;
1014- case VolumeControl.ActiveOutput.USB_SPEAKER:
1015- if (volume <= 0.0)
1016- icon = "audio-volume-muted-panel";
1017- else if (volume <= 0.3)
1018- icon = "audio-volume-low-panel";
1019- else if (volume <= 0.7)
1020- icon = "audio-volume-medium-panel";
1021- else
1022- icon = "audio-volume-high-panel";
1023- break;
1024- case VolumeControl.ActiveOutput.USB_HEADPHONES:
1025- if (volume <= 0.0)
1026- icon = "audio-volume-muted-panel";
1027- else if (volume <= 0.3)
1028- icon = "audio-volume-low-panel";
1029- else if (volume <= 0.7)
1030- icon = "audio-volume-medium-panel";
1031- else
1032- icon = "audio-volume-high-panel";
1033- break;
1034- case VolumeControl.ActiveOutput.HDMI_SPEAKER:
1035- if (volume <= 0.0)
1036- icon = "audio-volume-muted-panel";
1037- else if (volume <= 0.3)
1038- icon = "audio-volume-low-panel";
1039- else if (volume <= 0.7)
1040- icon = "audio-volume-medium-panel";
1041- else
1042- icon = "audio-volume-high-panel";
1043- break;
1044- case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
1045- if (volume <= 0.0)
1046- icon = "audio-volume-muted-panel";
1047- else if (volume <= 0.3)
1048- icon = "audio-volume-low-panel";
1049- else if (volume <= 0.7)
1050- icon = "audio-volume-medium-panel";
1051- else
1052- icon = "audio-volume-high-panel";
1053- break;
1054- }
1055- return icon;
1056- }
1057-
1058- private string get_volume_notification_icon (double volume, bool loud, VolumeControl.ActiveOutput active_output) {
1059- string icon = "";
1060- if (loud) {
1061- switch (active_output)
1062- {
1063- case VolumeControl.ActiveOutput.SPEAKERS:
1064- icon = "audio-volume-high";
1065- break;
1066- case VolumeControl.ActiveOutput.HEADPHONES:
1067- icon = "audio-volume-high";
1068- break;
1069- case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
1070- icon = "audio-volume-high";
1071- break;
1072- case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER:
1073- icon = "audio-volume-high";
1074- break;
1075- case VolumeControl.ActiveOutput.USB_SPEAKER:
1076- icon = "audio-volume-high";
1077- break;
1078- case VolumeControl.ActiveOutput.USB_HEADPHONES:
1079- icon = "audio-volume-high";
1080- break;
1081- case VolumeControl.ActiveOutput.HDMI_SPEAKER:
1082- icon = "audio-volume-high";
1083- break;
1084- case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
1085- icon = "audio-volume-high";
1086- break;
1087- }
1088- } else {
1089- icon = get_volume_icon (volume, active_output);
1090- }
1091- return icon;
1092- }
1093-
1094- private string get_volume_root_icon (double volume, bool mute, VolumeControl.ActiveOutput active_output) {
1095- string icon = "";
1096- switch (active_output)
1097- {
1098- case VolumeControl.ActiveOutput.SPEAKERS:
1099- if (mute || volume <= 0.0)
1100- icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel";
1101- else if (this.accounts_service != null && this.accounts_service.silentMode)
1102- icon = "audio-volume-muted-panel";
1103- else
1104- icon = get_volume_root_icon_by_volume (volume, active_output);
1105- break;
1106- case VolumeControl.ActiveOutput.HEADPHONES:
1107- if (mute || volume <= 0.0)
1108- icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel";
1109- else if (this.accounts_service != null && this.accounts_service.silentMode)
1110- icon = "audio-volume-muted-panel";
1111- else
1112- icon = get_volume_root_icon_by_volume (volume, active_output);
1113- break;
1114- case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
1115- if (mute || volume <= 0.0)
1116- icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel";
1117- else if (this.accounts_service != null && this.accounts_service.silentMode)
1118- icon = "audio-volume-muted-panel";
1119- else
1120- icon = get_volume_root_icon_by_volume (volume, active_output);
1121- break;
1122- case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER:
1123- if (mute || volume <= 0.0)
1124- icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel";
1125- else if (this.accounts_service != null && this.accounts_service.silentMode)
1126- icon = "audio-volume-muted-panel";
1127- else
1128- icon = get_volume_root_icon_by_volume (volume, active_output);
1129- break;
1130- case VolumeControl.ActiveOutput.USB_SPEAKER:
1131- if (mute || volume <= 0.0)
1132- icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel";
1133- else if (this.accounts_service != null && this.accounts_service.silentMode)
1134- icon = "audio-volume-muted-panel";
1135- else
1136- icon = get_volume_root_icon_by_volume (volume, active_output);
1137- break;
1138- case VolumeControl.ActiveOutput.USB_HEADPHONES:
1139- if (mute || volume <= 0.0)
1140- icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel";
1141- else if (this.accounts_service != null && this.accounts_service.silentMode)
1142- icon = "audio-volume-muted-panel";
1143- else
1144- icon = get_volume_root_icon_by_volume (volume, active_output);
1145- break;
1146- case VolumeControl.ActiveOutput.HDMI_SPEAKER:
1147- if (mute || volume <= 0.0)
1148- icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel";
1149- else if (this.accounts_service != null && this.accounts_service.silentMode)
1150- icon = "audio-volume-muted-panel";
1151- else
1152- icon = get_volume_root_icon_by_volume (volume, active_output);
1153- break;
1154- case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
1155- if (mute || volume <= 0.0)
1156- icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel";
1157- else if (this.accounts_service != null && this.accounts_service.silentMode)
1158- icon = "audio-volume-muted-panel";
1159- else
1160- icon = get_volume_root_icon_by_volume (volume, active_output);
1161- break;
1162- }
1163- return icon;
1164- }
1165-
1166- private string get_notification_label () {
1167- string volume_label = "";
1168- switch (volume_control.active_output)
1169- {
1170- case VolumeControl.ActiveOutput.SPEAKERS:
1171- volume_label = _("Speakers");
1172- break;
1173- case VolumeControl.ActiveOutput.HEADPHONES:
1174- volume_label = _("Headphones");
1175- break;
1176- case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
1177- volume_label = _("Bluetooth headphones");
1178- break;
1179- case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER:
1180- volume_label = _("Bluetooth speaker");
1181- break;
1182- case VolumeControl.ActiveOutput.USB_SPEAKER:
1183- volume_label = _("Usb speaker");
1184- break;
1185- case VolumeControl.ActiveOutput.USB_HEADPHONES:
1186- volume_label = _("Usb headphones");
1187- break;
1188- case VolumeControl.ActiveOutput.HDMI_SPEAKER:
1189- volume_label = _("HDMI speaker");
1190- break;
1191- case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
1192- volume_label = _("HDMI headphones");
1193- break;
1194- }
1195-
1196- return volume_label;
1197+
1198+ private static unowned string get_volume_root_icon_by_volume (double volume, VolumeControl.ActiveOutput active_output) {
1199+ switch (active_output) {
1200+ case VolumeControl.ActiveOutput.SPEAKERS:
1201+ case VolumeControl.ActiveOutput.HEADPHONES:
1202+ case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
1203+ case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER:
1204+ case VolumeControl.ActiveOutput.USB_SPEAKER:
1205+ case VolumeControl.ActiveOutput.USB_HEADPHONES:
1206+ case VolumeControl.ActiveOutput.HDMI_SPEAKER:
1207+ case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
1208+ if (volume <= 0.0)
1209+ return "audio-volume-muted-panel";
1210+ if (volume <= 0.3)
1211+ return "audio-volume-low-panel";
1212+ if (volume <= 0.7)
1213+ return "audio-volume-medium-panel";
1214+ return "audio-volume-high-panel";
1215+
1216+ default:
1217+ return "";
1218+ }
1219+ }
1220+
1221+ private unowned string get_volume_root_icon (double volume, bool mute, VolumeControl.ActiveOutput active_output) {
1222+ switch (active_output) {
1223+ case VolumeControl.ActiveOutput.SPEAKERS:
1224+ case VolumeControl.ActiveOutput.HEADPHONES:
1225+ case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
1226+ case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER:
1227+ case VolumeControl.ActiveOutput.USB_SPEAKER:
1228+ case VolumeControl.ActiveOutput.USB_HEADPHONES:
1229+ case VolumeControl.ActiveOutput.HDMI_SPEAKER:
1230+ case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
1231+ if (mute || volume <= 0.0)
1232+ return this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel";
1233+ if (this.accounts_service != null && this.accounts_service.silentMode)
1234+ return "audio-volume-muted-panel";
1235+ return get_volume_root_icon_by_volume (volume, active_output);
1236+
1237+ default:
1238+ return "";
1239+ }
1240 }
1241
1242 private void update_notification () {
1243-
1244- List<string> caps = Notify.get_server_caps ();
1245- notify_server_supports_actions = caps.find_custom ("actions", strcmp) != null;
1246- notify_server_supports_sync = caps.find_custom ("x-canonical-private-synchronous", strcmp) != null;
1247- notify_server_caps_checked = true;
1248-
1249- var loud = volume_control.high_volume;
1250- bool ignore_warning_this_time = this.volume_control.ignore_high_volume;
1251- var warn = loud
1252- && this.notify_server_supports_actions
1253- && !this.volume_control.high_volume_approved
1254- && !ignore_warning_this_time;
1255- if (waiting_user_approve_warn && volume_control.below_warning_volume) {
1256- volume_control.set_warning_volume();
1257- close_notification(warn_notification);
1258- }
1259- if (warn) {
1260- close_notification(info_notification);
1261- if (_pre_warn_volume == null) {
1262- _pre_warn_volume = new VolumeControl.Volume();
1263- _pre_warn_volume.volume = volume_control.volume.volume;
1264- _pre_warn_volume.reason = volume_control.volume.reason;
1265- }
1266- warn_notification.clear_actions();
1267- warn_notification.add_action ("ok", _("OK"), (n, a) => {
1268- stop_clamp_to_high_timeout();
1269- volume_control.approve_high_volume ();
1270- // restore the volume the user introduced
1271- VolumeControl.Volume vol = new VolumeControl.Volume();
1272- vol.volume = volume_control.get_pre_clamped_volume();
1273- vol.reason = VolumeControl.VolumeReasons.USER_KEYPRESS;
1274- _pre_warn_volume = null;
1275- volume_control.volume = vol;
1276-
1277- waiting_user_approve_warn = false;
1278- });
1279- warn_notification.add_action ("cancel", _("Cancel"), (n, a) => {
1280- _pre_warn_volume = null;
1281- waiting_user_approve_warn = false;
1282- increment_volume_sync_action();
1283- });
1284- waiting_user_approve_warn = true;
1285- show_notification(warn_notification);
1286- } else {
1287- if (!waiting_user_approve_warn) {
1288- close_notification(warn_notification);
1289-
1290- if (notify_server_supports_sync && !block_info_notifications && !ignore_warning_this_time) {
1291- /* Determine Label */
1292- string volume_label = get_notification_label ();
1293-
1294- /* Choose an icon */
1295- string icon = get_volume_notification_icon (volume_control.volume.volume, loud, volume_control.active_output);
1296-
1297- /* Reset the notification */
1298- var n = this.info_notification;
1299- n.update (_("Volume"), volume_label, icon);
1300- n.clear_hints();
1301- n.set_hint ("x-canonical-non-shaped-icon", "true");
1302- n.set_hint ("x-canonical-private-synchronous", "true");
1303- n.set_hint ("x-canonical-value-bar-tint", loud ? "true" : "false");
1304- n.set_hint ("value", (int32)Math.round(get_volume_percent() * 100.0));
1305- show_notification(n);
1306- }
1307- }
1308+ if (!_volume_warning.active && !block_info_notifications) {
1309+ _info_notification.show(this.volume_control.active_output(),
1310+ get_volume_percent(),
1311+ _volume_warning.high_volume);
1312 }
1313 }
1314
1315@@ -738,7 +394,7 @@
1316 this.mute_blocks_sound = false;
1317 this.sound_was_blocked_timeout_id = 0;
1318 this.update_root_icon ();
1319- return false;
1320+ return Source.REMOVE;
1321 });
1322 }
1323
1324@@ -750,10 +406,10 @@
1325
1326 /* return the current volume in the range of [0.0, 1.0] */
1327 private double get_volume_percent() {
1328- return volume_control.volume.volume / this.volume_control.max_volume;
1329+ return volume_control.volume.volume / _options.max_volume;
1330 }
1331
1332- /* volume control's range can vary depending on its max_volume property,
1333+ /* volume control's range can vary depending on options.max_volume,
1334 * but the action always needs to be in [0.0, 1.0]... */
1335 private Variant create_volume_action_state() {
1336 return new Variant.double (get_volume_percent());
1337@@ -768,14 +424,14 @@
1338 volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, create_volume_action_state());
1339
1340 volume_action.change_state.connect ( (action, val) => {
1341- double v = val.get_double () * this.volume_control.max_volume;
1342+ double v = val.get_double () * _options.max_volume;
1343 volume_control.set_volume_clamp (v, VolumeControl.VolumeReasons.USER_KEYPRESS);
1344 });
1345
1346 /* activating this action changes the volume by the amount given in the parameter */
1347 volume_action.activate.connect ((a,p) => activate_scroll_action(a,p));
1348
1349- this.volume_control.notify["max-volume"].connect(() => {
1350+ _options.notify["max-volume"].connect(() => {
1351 update_volume_action_state();
1352 });
1353
1354@@ -788,9 +444,6 @@
1355 if (reason == VolumeControl.VolumeReasons.USER_KEYPRESS ||
1356 reason == VolumeControl.VolumeReasons.DEVICE_OUTPUT_CHANGE)
1357 this.update_notification ();
1358-
1359- if ((warn_notification.id != 0) && (_pre_warn_volume != null))
1360- clamp_to_high_soon();
1361 });
1362
1363 this.volume_control.bind_property ("ready", volume_action, "enabled", BindingFlags.SYNC_CREATE);
1364@@ -815,12 +468,19 @@
1365 return mic_volume_action;
1366 }
1367
1368+ private Variant create_high_volume_action_state() {
1369+ return new Variant.boolean (_volume_warning.high_volume);
1370+ }
1371+ private void update_high_volume_action_state() {
1372+ high_volume_action.set_state(create_high_volume_action_state());
1373+ }
1374+
1375 SimpleAction high_volume_action;
1376 Action create_high_volume_action () {
1377- high_volume_action = new SimpleAction.stateful("high-volume", null, new Variant.boolean (this.volume_control.high_volume));
1378+ high_volume_action = new SimpleAction.stateful("high-volume", null, create_high_volume_action_state());
1379
1380- this.volume_control.notify["high-volume"].connect( () => {
1381- high_volume_action.set_state(new Variant.boolean (this.volume_control.high_volume));
1382+ _volume_warning.notify["high-volume"].connect( () => {
1383+ update_high_volume_action_state();
1384 update_notification();
1385 });
1386
1387@@ -842,7 +502,7 @@
1388 uint export_actions = 0;
1389
1390 Variant action_state_for_player (MediaPlayer player, bool show_track = true) {
1391- var builder = new VariantBuilder (new VariantType ("a{sv}"));
1392+ var builder = new VariantBuilder (VariantType.VARDICT);
1393 builder.add ("{sv}", "running", new Variant ("b", player.is_running));
1394 builder.add ("{sv}", "state", new Variant ("s", player.state));
1395 if (player.current_track != null && show_track) {
1396@@ -881,7 +541,7 @@
1397 clear_acts_player();
1398
1399 this.player_action_update_id = 0;
1400- return false;
1401+ return Source.REMOVE;
1402 }
1403
1404 void eventually_update_player_actions () {
1405@@ -956,27 +616,4 @@
1406
1407 this.update_preferred_players ();
1408 }
1409-
1410- /** VOLUME CLAMPING **/
1411-
1412- private uint _clamp_to_high_timeout = 0;
1413-
1414- private void stop_clamp_to_high_timeout() {
1415- if (_clamp_to_high_timeout != 0) {
1416- Source.remove(_clamp_to_high_timeout);
1417- _clamp_to_high_timeout = 0;
1418- }
1419- }
1420-
1421- private void clamp_to_high_soon() {
1422- const uint interval_msec = 200;
1423- if (_clamp_to_high_timeout == 0)
1424- _clamp_to_high_timeout = Timeout.add(interval_msec, clamp_to_high_idle);
1425- }
1426-
1427- private bool clamp_to_high_idle() {
1428- _clamp_to_high_timeout = 0;
1429- volume_control.clamp_to_high_volume();
1430- return false; // Source.REMOVE;
1431- }
1432 }
1433
1434=== modified file 'src/volume-control-pulse.vala'
1435--- src/volume-control-pulse.vala 2016-01-05 15:08:24 +0000
1436+++ src/volume-control-pulse.vala 2016-01-13 20:35:42 +0000
1437@@ -22,9 +22,6 @@
1438 using Notify;
1439 using Gee;
1440
1441-[CCode(cname="pa_cvolume_set", cheader_filename = "pulse/volume.h")]
1442-extern unowned PulseAudio.CVolume? vol_set (PulseAudio.CVolume? cv, uint channels, PulseAudio.Volume v);
1443-
1444 [DBus (name="com.canonical.UnityGreeter.List")]
1445 interface GreeterListInterface : Object
1446 {
1447@@ -34,19 +31,14 @@
1448
1449 public class VolumeControlPulse : VolumeControl
1450 {
1451- /* this is static to ensure it being freed after @context (loop does not have ref counting) */
1452- private static PulseAudio.GLibMainLoop loop;
1453+ private unowned PulseAudio.GLibMainLoop loop = null;
1454
1455 private uint _reconnect_timer = 0;
1456
1457 private PulseAudio.Context context;
1458 private bool _mute = true;
1459- private bool _is_playing = false;
1460- private bool _ignore_warning_this_time = false;
1461 private VolumeControl.Volume _volume = new VolumeControl.Volume();
1462 private double _mic_volume = 0.0;
1463- private Settings _settings = new Settings ("com.canonical.indicator.sound");
1464- private Settings _shared_settings = new Settings ("com.ubuntu.sound");
1465
1466 /* Used by the pulseaudio stream restore extension */
1467 private DBusConnection _pconn;
1468@@ -57,22 +49,6 @@
1469 private bool _pulse_use_stream_restore = false;
1470 private int32 _active_sink_input = -1;
1471 private string[] _valid_roles = {"multimedia", "alert", "alarm", "phone"};
1472- public override string stream {
1473- get {
1474- if (_active_sink_input == -1)
1475- return "alert";
1476- var path = _sink_input_hash[_active_sink_input];
1477- if (path == _objp_role_multimedia)
1478- return "multimedia";
1479- if (path == _objp_role_alert)
1480- return "alert";
1481- if (path == _objp_role_alarm)
1482- return "alarm";
1483- if (path == _objp_role_phone)
1484- return "phone";
1485- return "alert";
1486- }
1487- }
1488 private string? _objp_role_multimedia = null;
1489 private string? _objp_role_alert = null;
1490 private string? _objp_role_alarm = null;
1491@@ -87,40 +63,28 @@
1492 private uint _accountservice_volume_timer = 0;
1493 private bool _send_next_local_volume = false;
1494 private double _account_service_volume = 0.0;
1495- private bool _active_port_headphone = false;
1496 private VolumeControl.ActiveOutput _active_output = VolumeControl.ActiveOutput.SPEAKERS;
1497
1498- /** true when connected to the pulse server */
1499- public override bool ready { get; private set; }
1500-
1501 /** true when a microphone is active **/
1502 public override bool active_mic { get; private set; default = false; }
1503
1504- public VolumeControlPulse ()
1505+ public VolumeControlPulse (IndicatorSound.Options options, PulseAudio.GLibMainLoop loop)
1506 {
1507+ base(options);
1508+
1509 _volume.volume = 0.0;
1510 _volume.reason = VolumeControl.VolumeReasons.PULSE_CHANGE;
1511
1512- if (loop == null)
1513- loop = new PulseAudio.GLibMainLoop ();
1514+ this.loop = loop;
1515
1516 _mute_cancellable = new Cancellable ();
1517 _volume_cancellable = new Cancellable ();
1518
1519- init_all_properties();
1520-
1521 setup_accountsservice.begin ();
1522
1523 this.reconnect_to_pulse ();
1524 }
1525
1526- private void init_all_properties()
1527- {
1528- init_max_volume();
1529- init_high_volume();
1530- init_high_volume_approved();
1531- }
1532-
1533 ~VolumeControlPulse ()
1534 {
1535 stop_all_timers();
1536@@ -134,10 +98,9 @@
1537 }
1538 stop_local_volume_timer();
1539 stop_account_service_volume_timer();
1540- stop_high_volume_approved_timer();
1541 }
1542
1543- private VolumeControl.ActiveOutput calculate_active_output (SinkInfo? sink) {
1544+ public static VolumeControl.ActiveOutput calculate_active_output (SinkInfo? sink) {
1545
1546 VolumeControl.ActiveOutput ret_output = VolumeControl.ActiveOutput.SPEAKERS;
1547 /* Check if the current active port is headset/headphone */
1548@@ -155,9 +118,8 @@
1549 (sink.active_port != null &&
1550 (sink.active_port.name.contains("headset") ||
1551 sink.active_port.name.contains("headphone")))) {
1552- _active_port_headphone = true;
1553 // check if it's a bluetooth device
1554- var device_bus = sink.proplist.gets ("device.bus");
1555+ unowned string device_bus = sink.proplist.gets ("device.bus");
1556 if (device_bus != null && device_bus == "bluetooth") {
1557 ret_output = VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES;
1558 } else if (device_bus != null && device_bus == "usb") {
1559@@ -169,8 +131,7 @@
1560 }
1561 } else {
1562 // speaker
1563- _active_port_headphone = false;
1564- var device_bus = sink.proplist.gets ("device.bus");
1565+ unowned string device_bus = sink.proplist.gets ("device.bus");
1566 if (device_bus != null && device_bus == "bluetooth") {
1567 ret_output = VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER;
1568 } else if (device_bus != null && device_bus == "usb") {
1569@@ -245,28 +206,19 @@
1570 }
1571
1572 var playing = (i.state == PulseAudio.SinkState.RUNNING);
1573- if (_is_playing != playing)
1574- {
1575- _is_playing = playing;
1576- this.notify_property ("is-playing");
1577- }
1578-
1579- // store the current status of the active output
1580- VolumeControl.ActiveOutput active_output_before = active_output;
1581-
1582- // calculate the output
1583- _active_output = calculate_active_output (i);
1584-
1585- // check if the output has changed, if so... emit a signal
1586- VolumeControl.ActiveOutput active_output_now = active_output;
1587- if (active_output_now != active_output_before &&
1588- (active_output_now != VolumeControl.ActiveOutput.CALL_MODE &&
1589- active_output_before != VolumeControl.ActiveOutput.CALL_MODE)) {
1590- this.active_output_changed (active_output_now);
1591- if (active_output_now == VolumeControl.ActiveOutput.SPEAKERS) {
1592- _high_volume_approved = false;
1593- }
1594- update_high_volume();
1595+ if (is_playing != playing)
1596+ is_playing = playing;
1597+
1598+ var oldval = _active_output;
1599+ var newval = calculate_active_output(i);
1600+
1601+ _active_output = newval;
1602+
1603+ // Emit a change signal iff CALL_MODE wasn't involved. (FIXME: yuck.)
1604+ if ((oldval != VolumeControl.ActiveOutput.CALL_MODE) &&
1605+ (newval != VolumeControl.ActiveOutput.CALL_MODE) &&
1606+ (oldval != newval)) {
1607+ this.active_output_changed (newval);
1608 }
1609
1610 if (_pulse_use_stream_restore == false &&
1611@@ -345,10 +297,6 @@
1612 var vol = new VolumeControl.Volume();
1613 vol.volume = volume_to_double (lvolume);
1614 vol.reason = VolumeControl.VolumeReasons.PULSE_CHANGE;
1615- // Ignore changes from PULSE to avoid issues with
1616- // some apps that change the volume in the sink
1617- // We only take into account volume changes from the user
1618- this._ignore_warning_this_time = true;
1619 this.volume = vol;
1620 }
1621 }
1622@@ -358,6 +306,21 @@
1623 return message;
1624 }
1625
1626+ private VolumeControl.Stream calculate_active_stream()
1627+ {
1628+ if (_active_sink_input != -1) {
1629+ var path = _sink_input_hash[_active_sink_input];
1630+ if (path == _objp_role_multimedia)
1631+ return Stream.MULTIMEDIA;
1632+ if (path == _objp_role_alarm)
1633+ return Stream.ALARM;
1634+ if (path == _objp_role_phone)
1635+ return Stream.PHONE;
1636+ }
1637+
1638+ return VolumeControl.Stream.ALERT;
1639+ }
1640+
1641 private async void update_active_sink_input (int32 index)
1642 {
1643 if ((index == -1) || (index != _active_sink_input && index in _sink_input_list)) {
1644@@ -365,10 +328,14 @@
1645 if (index != -1)
1646 sink_input_objp = _sink_input_hash.get (index);
1647 _active_sink_input = index;
1648+ var stream = calculate_active_stream();
1649+ if (active_stream != stream) {
1650+ active_stream = stream;
1651+ }
1652
1653 /* Listen for role volume changes from pulse itself (external clients) */
1654 try {
1655- var builder = new VariantBuilder (new VariantType ("ao"));
1656+ var builder = new VariantBuilder (VariantType.OBJECT_PATH_ARRAY);
1657 builder.add ("o", sink_input_objp);
1658
1659 yield _pconn.call ("org.PulseAudio.Core1", "/org/pulseaudio/core1",
1660@@ -393,10 +360,6 @@
1661 var vol = new VolumeControl.Volume();
1662 vol.volume = volume_to_double (volume);
1663 vol.reason = VolumeControl.VolumeReasons.VOLUME_STREAM_CHANGE;
1664- // Ignore changes from PULSE to avoid issues with
1665- // some apps that change the volume in the sink
1666- // We only take into account volume changes from the user
1667- this._ignore_warning_this_time = true;
1668 this.volume = vol;
1669 } catch (GLib.Error e) {
1670 warning ("unable to get volume for active role %s (%s)", sink_input_objp, e.message);
1671@@ -407,7 +370,7 @@
1672 private void add_sink_input_into_list (SinkInputInfo sink_input)
1673 {
1674 /* We're only adding ones that are not corked and with a valid role */
1675- var role = sink_input.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
1676+ unowned string role = sink_input.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
1677
1678 if (role != null && role in _valid_roles) {
1679 if (sink_input.corked == 0 || role == "phone") {
1680@@ -477,7 +440,7 @@
1681 if (i == null)
1682 return;
1683
1684- var role = i.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
1685+ unowned string role = i.proplist.gets (PulseAudio.Proplist.PROP_MEDIA_ROLE);
1686 if (role == "phone" || role == "production")
1687 this.active_mic = true;
1688 }
1689@@ -499,7 +462,7 @@
1690 c.set_subscribe_callback (context_events_cb);
1691 update_sink ();
1692 update_source ();
1693- this.ready = true;
1694+ this.ready = true; // true because we're connected to the pulse server
1695 break;
1696
1697 case Context.State.FAILED:
1698@@ -518,7 +481,7 @@
1699 {
1700 _reconnect_timer = 0;
1701 reconnect_to_pulse ();
1702- return false; // G_SOURCE_REMOVE
1703+ return Source.REMOVE;
1704 }
1705
1706 void reconnect_to_pulse ()
1707@@ -540,7 +503,7 @@
1708 this.context = new PulseAudio.Context (loop.get_api(), null, props);
1709 this.context.set_state_callback (context_state_callback);
1710
1711- var server_string = Environment.get_variable("PULSE_SERVER");
1712+ unowned string server_string = Environment.get_variable("PULSE_SERVER");
1713 if (context.connect(server_string, Context.Flags.NOFAIL, null) < 0)
1714 warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno()));
1715 }
1716@@ -590,30 +553,19 @@
1717 }
1718 }
1719
1720- public override bool is_playing
1721+ public override VolumeControl.ActiveOutput active_output()
1722 {
1723- get
1724- {
1725- return this._is_playing;
1726- }
1727- }
1728-
1729- public override VolumeControl.ActiveOutput active_output
1730- {
1731- get
1732- {
1733- return _active_output;
1734- }
1735+ return _active_output;
1736 }
1737
1738 /* Volume operations */
1739- private static PulseAudio.Volume double_to_volume (double vol)
1740+ public static PulseAudio.Volume double_to_volume (double vol)
1741 {
1742 double tmp = (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED) * vol;
1743 return (PulseAudio.Volume)tmp + PulseAudio.Volume.MUTED;
1744 }
1745
1746- private static double volume_to_double (PulseAudio.Volume vol)
1747+ public static double volume_to_double (PulseAudio.Volume vol)
1748 {
1749 double tmp = (double)(vol - PulseAudio.Volume.MUTED);
1750 return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED);
1751@@ -668,8 +620,6 @@
1752 active_role_objp, "org.freedesktop.DBus.Properties", "Set",
1753 new Variant ("(ssv)", "org.PulseAudio.Ext.StreamRestore1.RestoreEntry", "Volume", volume),
1754 null, DBusCallFlags.NONE, -1);
1755-
1756- debug ("Set volume to %f on path %s", vol, active_role_objp);
1757 } catch (GLib.Error e) {
1758 lock (_pa_volume_sig_count) {
1759 _pa_volume_sig_count--;
1760@@ -687,7 +637,7 @@
1761 void set_mic_volume_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) {
1762 if (i != null) {
1763 unowned CVolume cvol = CVolume ();
1764- cvol = vol_set (cvol, 1, double_to_volume (_mic_volume));
1765+ cvol.set (1, double_to_volume (_mic_volume));
1766 c.set_source_volume_by_name (i.default_source_name, cvol, set_mic_volume_success_cb);
1767 }
1768 }
1769@@ -716,169 +666,8 @@
1770 && volume_changed) {
1771 start_local_volume_timer();
1772 }
1773-
1774- update_high_volume();
1775- }
1776- }
1777-
1778- /** MAX VOLUME PROPERTY **/
1779-
1780- private void init_max_volume() {
1781- _settings.changed["normal-volume-decibels"].connect(() => update_max_volume());
1782- _settings.changed["amplified-volume-decibels"].connect(() => update_max_volume());
1783- _shared_settings.changed["allow-amplified-volume"].connect(() => update_max_volume());
1784- update_max_volume();
1785- }
1786- private void update_max_volume () {
1787- var new_max_volume = calculate_max_volume();
1788- if (max_volume != new_max_volume) {
1789- debug("changing max_volume from %f to %f", this.max_volume, new_max_volume);
1790- max_volume = calculate_max_volume();
1791- }
1792- }
1793- private double calculate_max_volume () {
1794- unowned string decibel_key = _shared_settings.get_boolean("allow-amplified-volume")
1795- ? "amplified-volume-decibels"
1796- : "normal-volume-decibels";
1797- var volume_dB = _settings.get_double(decibel_key);
1798- var volume_sw = PulseAudio.Volume.sw_from_dB (volume_dB);
1799- return volume_to_double (volume_sw);
1800- }
1801-
1802- /** HIGH VOLUME PROPERTY **/
1803-
1804- private bool _warning_volume_enabled;
1805- private double _warning_volume_norms; /* 1.0 == PA_VOLUME_NORM */
1806- private bool _high_volume = false;
1807- public override bool ignore_high_volume {
1808- get {
1809- if (_ignore_warning_this_time) {
1810- warning("Ignore");
1811- _ignore_warning_this_time = false;
1812- return true;
1813- }
1814- return false;
1815- }
1816- set { }
1817- }
1818- public override bool high_volume {
1819- get { return this._high_volume; }
1820- private set { this._high_volume = value; }
1821- }
1822- public override bool below_warning_volume {
1823- get { return this._volume.volume < this._warning_volume_norms; }
1824- private set { }
1825- }
1826- private void init_high_volume() {
1827- _settings.changed["warning-volume-enabled"].connect(() => update_high_volume_cache());
1828- _settings.changed["warning-volume-decibels"].connect(() => update_high_volume_cache());
1829- update_high_volume_cache();
1830- }
1831- private void update_high_volume_cache() {
1832- var volume_dB = _settings.get_double ("warning-volume-decibels");
1833- var volume_sw = PulseAudio.Volume.sw_from_dB (volume_dB);
1834- var volume_norms = volume_to_double (volume_sw);
1835- _warning_volume_norms = volume_norms;
1836- _warning_volume_enabled = _settings.get_boolean("warning-volume-enabled");
1837- debug("updating high volume cache... enabled %d dB %f sw %lu norm %f", (int)_warning_volume_enabled, volume_dB, volume_sw, volume_norms);
1838- update_high_volume();
1839- }
1840- private void update_high_volume() {
1841- var new_high_volume = calculate_high_volume();
1842- if (high_volume != new_high_volume) {
1843- debug("changing high_volume from %d to %d", (int)high_volume, (int)new_high_volume);
1844- high_volume = new_high_volume;
1845- }
1846- }
1847- private bool calculate_high_volume() {
1848- return calculate_high_volume_from_volume(_volume.volume);
1849- }
1850- private bool calculate_high_volume_from_volume(double volume) {
1851- return _active_port_headphone
1852- && _warning_volume_enabled
1853- && volume > _warning_volume_norms
1854- && (stream == "multimedia");
1855- }
1856-
1857- public override void clamp_to_high_volume() {
1858- if (_high_volume && (_volume.volume > _warning_volume_norms)) {
1859- var vol = new VolumeControl.Volume();
1860- vol.volume = _volume.volume.clamp(0, _warning_volume_norms);
1861- vol.reason = _volume.reason;
1862- debug("Clamping from %f down to %f", _volume.volume, vol.volume);
1863- volume = vol;
1864- }
1865- }
1866-
1867- public override void set_warning_volume() {
1868- var vol = new VolumeControl.Volume();
1869- vol.volume = _warning_volume_norms;
1870- vol.reason = _volume.reason;
1871- debug("Setting warning level volume from %f down to %f", _volume.volume, vol.volume);
1872- volume = vol;
1873- }
1874-
1875- /** HIGH VOLUME APPROVED PROPERTY **/
1876-
1877- private bool _high_volume_approved = false;
1878- private uint _high_volume_approved_timer = 0;
1879- private int64 _high_volume_approved_at = 0;
1880- private int64 _high_volume_approved_ttl_usec = 0;
1881- public override bool high_volume_approved {
1882- get { return this._high_volume_approved; }
1883- private set { this._high_volume_approved = value; }
1884- }
1885- private void init_high_volume_approved() {
1886- _settings.changed["warning-volume-confirmation-ttl"].connect(() => update_high_volume_approved_cache());
1887- update_high_volume_approved_cache();
1888- }
1889- private void update_high_volume_approved_cache() {
1890- _high_volume_approved_ttl_usec = _settings.get_int("warning-volume-confirmation-ttl");
1891- _high_volume_approved_ttl_usec *= 1000000;
1892-
1893- update_high_volume_approved();
1894- update_high_volume_approved_timer();
1895- }
1896- private void update_high_volume_approved_timer() {
1897- stop_high_volume_approved_timer();
1898- if (_high_volume_approved_at != 0) {
1899- int64 expires_at = _high_volume_approved_at + _high_volume_approved_ttl_usec;
1900- int64 now = GLib.get_monotonic_time();
1901- if (expires_at > now) {
1902- var seconds_left = 1 + ((expires_at - now) / 1000000);
1903- _high_volume_approved_timer = Timeout.add_seconds((uint)seconds_left, on_high_volume_approved_timer);
1904- }
1905- }
1906- }
1907- private void stop_high_volume_approved_timer() {
1908- if (_high_volume_approved_timer != 0) {
1909- Source.remove (_high_volume_approved_timer);
1910- _high_volume_approved_timer = 0;
1911- }
1912- }
1913- private bool on_high_volume_approved_timer() {
1914- _high_volume_approved_timer = 0;
1915- update_high_volume_approved();
1916- return false; /* Source.REMOVE */
1917- }
1918- private void update_high_volume_approved() {
1919- var new_high_volume_approved = calculate_high_volume_approved();
1920- if (high_volume_approved != new_high_volume_approved) {
1921- debug("changing high_volume_approved from %d to %d", (int)high_volume_approved, (int)new_high_volume_approved);
1922- high_volume_approved = new_high_volume_approved;
1923- }
1924- }
1925- private bool calculate_high_volume_approved() {
1926- int64 now = GLib.get_monotonic_time();
1927- return (_high_volume_approved_at != 0)
1928- && (_high_volume_approved_at + _high_volume_approved_ttl_usec >= now);
1929- }
1930- public override void approve_high_volume() {
1931- _high_volume_approved_at = GLib.get_monotonic_time();
1932- update_high_volume_approved();
1933- update_high_volume_approved_timer();
1934- }
1935-
1936+ }
1937+ }
1938
1939 /** MIC VOLUME PROPERTY */
1940
1941@@ -895,16 +684,11 @@
1942 }
1943 }
1944
1945- /* PulseAudio Dbus (Stream Restore) logic */
1946- private void reconnect_pulse_dbus ()
1947+ public static DBusConnection? create_pulse_dbus_connection()
1948 {
1949 unowned string pulse_dbus_server_env = Environment.get_variable ("PULSE_DBUS_SERVER");
1950 string address;
1951
1952- /* In case of a reconnect */
1953- _pulse_use_stream_restore = false;
1954- _pa_volume_sig_count = 0;
1955-
1956 if (pulse_dbus_server_env != null) {
1957 address = pulse_dbus_server_env;
1958 } else {
1959@@ -915,7 +699,7 @@
1960 conn = Bus.get_sync (BusType.SESSION);
1961 } catch (GLib.IOError e) {
1962 warning ("unable to get the dbus session bus: %s", e.message);
1963- return;
1964+ return null;
1965 }
1966
1967 try {
1968@@ -927,27 +711,41 @@
1969 address = props.get_string ();
1970 } catch (GLib.Error e) {
1971 warning ("unable to get pulse unix socket: %s", e.message);
1972- return;
1973+ return null;
1974 }
1975 }
1976
1977- debug ("PulseAudio dbus unix socket: %s", address);
1978+ DBusConnection conn = null;
1979 try {
1980- _pconn = new DBusConnection.for_address_sync (address, DBusConnectionFlags.AUTHENTICATION_CLIENT);
1981+ conn = new DBusConnection.for_address_sync (address, DBusConnectionFlags.AUTHENTICATION_CLIENT);
1982 } catch (GLib.Error e) {
1983+ GLib.warning("Unable to connect to dbus server at '%s': %s", address, e.message);
1984 /* If it fails, it means the dbus pulse extension is not available */
1985+ }
1986+ GLib.debug ("PulseAudio dbus address is '%s', connection is '%p'", address, conn);
1987+ return conn;
1988+ }
1989+
1990+ /* PulseAudio Dbus (Stream Restore) logic */
1991+ private void reconnect_pulse_dbus ()
1992+ {
1993+ /* In case of a reconnect */
1994+ _pulse_use_stream_restore = false;
1995+ _pa_volume_sig_count = 0;
1996+
1997+ _pconn = create_pulse_dbus_connection();
1998+ if (_pconn == null)
1999 return;
2000- }
2001
2002 /* For pulse dbus related events */
2003 _pconn.add_filter (pulse_dbus_filter);
2004
2005 /* Check if the 4 currently supported media roles are already available in StreamRestore
2006 * Roles: multimedia, alert, alarm and phone */
2007- _objp_role_multimedia = stream_restore_get_object_path ("sink-input-by-media-role:multimedia");
2008- _objp_role_alert = stream_restore_get_object_path ("sink-input-by-media-role:alert");
2009- _objp_role_alarm = stream_restore_get_object_path ("sink-input-by-media-role:alarm");
2010- _objp_role_phone = stream_restore_get_object_path ("sink-input-by-media-role:phone");
2011+ _objp_role_multimedia = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:multimedia");
2012+ _objp_role_alert = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:alert");
2013+ _objp_role_alarm = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:alarm");
2014+ _objp_role_phone = stream_restore_get_object_path (_pconn, "sink-input-by-media-role:phone");
2015
2016 /* Only use stream restore if every used role is available */
2017 if (_objp_role_multimedia != null && _objp_role_alert != null && _objp_role_alarm != null && _objp_role_phone != null) {
2018@@ -958,10 +756,10 @@
2019 }
2020 }
2021
2022- private string? stream_restore_get_object_path (string name) {
2023+ public static string? stream_restore_get_object_path (DBusConnection pconn, string name) {
2024 string? objp = null;
2025 try {
2026- Variant props_variant = _pconn.call_sync ("org.PulseAudio.Ext.StreamRestore1",
2027+ Variant props_variant = pconn.call_sync ("org.PulseAudio.Ext.StreamRestore1",
2028 "/org/pulseaudio/stream_restore1", "org.PulseAudio.Ext.StreamRestore1",
2029 "GetEntryByName", new Variant ("(s)", name), null, DBusCallFlags.NONE, -1);
2030 /* Workaround for older versions of vala that don't provide get_objv */
2031@@ -977,7 +775,7 @@
2032 /* AccountsService operations */
2033 private void accountsservice_props_changed_cb (DBusProxy proxy, Variant changed_properties, string[]? invalidated_properties)
2034 {
2035- Variant volume_variant = changed_properties.lookup_value ("Volume", new VariantType ("d"));
2036+ Variant volume_variant = changed_properties.lookup_value ("Volume", VariantType.DOUBLE);
2037 if (volume_variant != null) {
2038 var volume = volume_variant.get_double ();
2039 if (volume >= 0) {
2040@@ -987,7 +785,7 @@
2041 }
2042 }
2043
2044- Variant mute_variant = changed_properties.lookup_value ("Muted", new VariantType ("b"));
2045+ Variant mute_variant = changed_properties.lookup_value ("Muted", VariantType.BOOLEAN);
2046 if (mute_variant != null) {
2047 var mute = mute_variant.get_boolean ();
2048 set_mute_internal (mute);
2049@@ -1061,7 +859,7 @@
2050 yield setup_user_proxy ();
2051 } else {
2052 // We are in a user session. We just need our own proxy
2053- var username = Environment.get_variable ("USER");
2054+ unowned string username = Environment.get_variable ("USER");
2055 if (username != "" && username != null) {
2056 yield setup_user_proxy (username);
2057 }
2058@@ -1128,7 +926,7 @@
2059 _send_next_local_volume = false;
2060 start_local_volume_timer ();
2061 }
2062- return false; // G_SOURCE_REMOVE
2063+ return Source.REMOVE;
2064 }
2065
2066 private void start_account_service_volume_timer()
2067@@ -1160,6 +958,6 @@
2068 {
2069 _accountservice_volume_timer = 0;
2070 start_account_service_volume_timer ();
2071- return false; // G_SOURCE_REMOVE
2072+ return Source.REMOVE;
2073 }
2074 }
2075
2076=== modified file 'src/volume-control.vala'
2077--- src/volume-control.vala 2016-01-05 15:08:24 +0000
2078+++ src/volume-control.vala 2016-01-13 20:35:42 +0000
2079@@ -40,36 +40,39 @@
2080 CALL_MODE
2081 }
2082
2083+ public enum Stream {
2084+ ALERT,
2085+ MULTIMEDIA,
2086+ ALARM,
2087+ PHONE
2088+ }
2089+
2090 public class Volume : Object {
2091 public double volume;
2092 public VolumeReasons reason;
2093 }
2094
2095- public virtual string stream { get { return ""; } }
2096- public virtual bool ready { get { return false; } set { } }
2097+ protected IndicatorSound.Options _options = null;
2098+
2099+ public VolumeControl(IndicatorSound.Options options) {
2100+ _options = options;
2101+ }
2102+
2103+ public Stream active_stream { get; protected set; default = Stream.ALERT; }
2104+ public bool ready { get; protected set; default = false; }
2105 public virtual bool active_mic { get { return false; } set { } }
2106- public virtual bool high_volume { get { return false; } protected set { } }
2107- public virtual bool ignore_high_volume { get { return false; } protected set { } }
2108- public virtual bool below_warning_volume { get { return false; } protected set { } }
2109 public virtual bool mute { get { return false; } }
2110- public virtual bool is_playing { get { return false; } }
2111- public virtual VolumeControl.ActiveOutput active_output { get { return VolumeControl.ActiveOutput.SPEAKERS; } }
2112+ public bool is_playing { get; protected set; default = false; }
2113 private Volume _volume;
2114 private double _pre_clamp_volume;
2115 public virtual Volume volume { get { return _volume; } set { } }
2116 public virtual double mic_volume { get { return 0.0; } set { } }
2117- public virtual double max_volume { get { return 1.0; } protected set { } }
2118-
2119- public virtual bool high_volume_approved { get { return false; } protected set { } }
2120- public virtual void approve_high_volume() { }
2121- public virtual void clamp_to_high_volume() { }
2122- public virtual void set_warning_volume() { }
2123
2124 public abstract void set_mute (bool mute);
2125
2126 public void set_volume_clamp (double unclamped, VolumeControl.VolumeReasons reason) {
2127 var v = new VolumeControl.Volume();
2128- v.volume = unclamped.clamp (0.0, this.max_volume);
2129+ v.volume = unclamped.clamp (0.0, _options.max_volume);
2130 v.reason = reason;
2131 this.volume = v;
2132 _pre_clamp_volume = unclamped;
2133@@ -79,5 +82,6 @@
2134 return _pre_clamp_volume;
2135 }
2136
2137+ public abstract VolumeControl.ActiveOutput active_output();
2138 public signal void active_output_changed (VolumeControl.ActiveOutput active_output);
2139 }
2140
2141=== added file 'src/volume-warning-pulse.vala'
2142--- src/volume-warning-pulse.vala 1970-01-01 00:00:00 +0000
2143+++ src/volume-warning-pulse.vala 2016-01-13 20:35:42 +0000
2144@@ -0,0 +1,211 @@
2145+/*
2146+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
2147+ *
2148+ * Copyright 2015 Canonical Ltd.
2149+ *
2150+ * This program is free software; you can redistribute it and/or modify
2151+ * it under the terms of the GNU General Public License as published by
2152+ * the Free Software Foundation; version 3.
2153+ *
2154+ * This program is distributed in the hope that it will be useful,
2155+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2156+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2157+ * GNU General Public License for more details.
2158+ *
2159+ * You should have received a copy of the GNU General Public License
2160+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2161+ *
2162+ * Authors:
2163+ * Charles Kerr <charles.kerr@canonical.com>
2164+ */
2165+
2166+using PulseAudio;
2167+
2168+/**
2169+ * A VolumeWarning that uses PulseAudio to
2170+ * (a) implement sound_system_set_multimedia_volume() and
2171+ * (b) keep the multimedia_active and multimedia_volume properties up-to-date
2172+ */
2173+public class VolumeWarningPulse : VolumeWarning
2174+{
2175+ public VolumeWarningPulse (IndicatorSound.Options options,
2176+ PulseAudio.GLibMainLoop pgloop) {
2177+ base (options);
2178+
2179+ _pgloop = pgloop;
2180+ pulse_reconnect ();
2181+ }
2182+
2183+ ~VolumeWarningPulse () {
2184+ clear_timer (ref _pulse_reconnect_timer);
2185+ clear_timer (ref _pending_sink_inputs_timer);
2186+ pulse_disconnect ();
2187+ }
2188+
2189+ protected override void preshow () {
2190+ /* showing the dialog can change the sink input index (bug #1484589)
2191+ * so cache it here for later use in sound_system_set_multimedia_volume() */
2192+ _target_sink_input_index = _multimedia_sink_input_index;
2193+ }
2194+
2195+ protected override void sound_system_set_multimedia_volume (PulseAudio.Volume volume) {
2196+ var index = _target_sink_input_index;
2197+
2198+ return_if_fail (_pulse_context != null);
2199+ return_if_fail (index != PulseAudio.INVALID_INDEX);
2200+ return_if_fail (volume != PulseAudio.Volume.INVALID);
2201+
2202+ unowned CVolume cvol = CVolume ();
2203+ cvol.set (1, volume);
2204+ debug ("setting multimedia (sink_input index %d) volume to %s", (int)index, cvol.to_string ());
2205+ _pulse_context.set_sink_input_volume (index, cvol);
2206+ }
2207+
2208+ private unowned PulseAudio.GLibMainLoop _pgloop = null;
2209+ private PulseAudio.Context _pulse_context = null;
2210+ private uint _pulse_reconnect_timer = 0;
2211+ private uint _pending_sink_inputs_timer = 0;
2212+ private GenericSet<uint32> _pending_sink_inputs = new GenericSet<uint32>(direct_hash, direct_equal);
2213+
2214+ private uint soon_interval_msec = 500;
2215+
2216+ private uint32 _target_sink_input_index = PulseAudio.INVALID_INDEX;
2217+ private uint32 _multimedia_sink_input_index = PulseAudio.INVALID_INDEX;
2218+
2219+ /***/
2220+
2221+ private bool is_active_multimedia (SinkInputInfo i) {
2222+ return (i.corked == 0) &&
2223+ (i.proplist.gets(PulseAudio.Proplist.PROP_MEDIA_ROLE) == "multimedia");
2224+
2225+ }
2226+
2227+ private void clear_multimedia () {
2228+ _multimedia_sink_input_index = PulseAudio.INVALID_INDEX;
2229+ multimedia_volume = PulseAudio.Volume.INVALID;
2230+ multimedia_active = false;
2231+ }
2232+
2233+ private void on_sink_input_info (Context c, SinkInputInfo? i, int eol) {
2234+
2235+ if (i == null)
2236+ return;
2237+
2238+ if (is_active_multimedia (i)) {
2239+ GLib.debug ("on_sink_input_info() setting multimedia sink input index to %d, sink index to %d", (int)i.index, (int)i.sink);
2240+ _multimedia_sink_input_index = i.index;
2241+ multimedia_volume = i.volume.max ();
2242+ multimedia_active = true;
2243+ }
2244+ else if (i.index == _multimedia_sink_input_index) {
2245+ clear_multimedia ();
2246+ }
2247+ }
2248+
2249+ private void update_all_sink_inputs () {
2250+ _pulse_context.get_sink_input_info_list (on_sink_input_info);
2251+ }
2252+ private void update_sink_input (uint32 index) {
2253+ _pulse_context.get_sink_input_info (index, on_sink_input_info);
2254+ }
2255+
2256+ private void update_sink_input_soon (uint32 index) {
2257+
2258+ _pending_sink_inputs.add (index);
2259+
2260+ if (_pending_sink_inputs_timer == 0) {
2261+ _pending_sink_inputs_timer = Timeout.add (soon_interval_msec, () => {
2262+ _pending_sink_inputs_timer = 0;
2263+ _pending_sink_inputs.foreach ((i) => update_sink_input (i));
2264+ _pending_sink_inputs.remove_all ();
2265+ return Source.REMOVE;
2266+ });
2267+ }
2268+ }
2269+
2270+ private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index) {
2271+ switch (t & Context.SubscriptionEventType.FACILITY_MASK)
2272+ {
2273+ case Context.SubscriptionEventType.SINK_INPUT:
2274+ switch (t & Context.SubscriptionEventType.TYPE_MASK)
2275+ {
2276+ // if a SinkInput changed, get its updated info
2277+ // to keep our multimedia indices up-to-date
2278+ case Context.SubscriptionEventType.NEW:
2279+ case Context.SubscriptionEventType.CHANGE:
2280+ update_sink_input_soon (index);
2281+ break;
2282+
2283+ // if the multimedia sink input was removed,
2284+ // reset our mm fields and look for a new mm sink input
2285+ case Context.SubscriptionEventType.REMOVE:
2286+ if (index == _multimedia_sink_input_index) {
2287+ clear_multimedia ();
2288+ update_all_sink_inputs ();
2289+ }
2290+ break;
2291+
2292+ default:
2293+ GLib.debug ("Sink input event not known.");
2294+ break;
2295+ }
2296+ break;
2297+
2298+ default:
2299+ break;
2300+ }
2301+ }
2302+
2303+ private void pulse_context_state_callback (Context c) {
2304+ switch (c.get_state ()) {
2305+ case Context.State.READY:
2306+ c.set_subscribe_callback (context_events_cb);
2307+ c.subscribe (PulseAudio.Context.SubscriptionMask.SINK |
2308+ PulseAudio.Context.SubscriptionMask.SINK_INPUT);
2309+ update_all_sink_inputs ();
2310+ break;
2311+
2312+ case Context.State.FAILED:
2313+ case Context.State.TERMINATED:
2314+ pulse_reconnect_soon ();
2315+ break;
2316+
2317+ default:
2318+ break;
2319+ }
2320+ }
2321+
2322+ private void pulse_disconnect () {
2323+ if (_pulse_context != null) {
2324+ _pulse_context.disconnect ();
2325+ _pulse_context = null;
2326+ }
2327+ }
2328+
2329+ private void pulse_reconnect_soon () {
2330+ if (_pulse_reconnect_timer == 0) {
2331+ _pulse_reconnect_timer = Timeout.add_seconds (2, () => {
2332+ _pulse_reconnect_timer = 0;
2333+ pulse_reconnect ();
2334+ return Source.REMOVE;
2335+ });
2336+ }
2337+ }
2338+
2339+ void pulse_reconnect () {
2340+ pulse_disconnect ();
2341+
2342+ var props = new Proplist ();
2343+ props.sets (Proplist.PROP_APPLICATION_NAME, "Ubuntu Audio Settings");
2344+ props.sets (Proplist.PROP_APPLICATION_ID, "com.canonical.settings.sound");
2345+ props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control");
2346+ props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1");
2347+
2348+ _pulse_context = new PulseAudio.Context (_pgloop.get_api(), null, props);
2349+ _pulse_context.set_state_callback (pulse_context_state_callback);
2350+
2351+ unowned string server_string = Environment.get_variable ("PULSE_SERVER");
2352+ if (_pulse_context.connect (server_string, Context.Flags.NOFAIL, null) < 0)
2353+ GLib.warning ("pa_context_connect() failed: %s\n", PulseAudio.strerror(_pulse_context.errno()));
2354+ }
2355+}
2356
2357=== added file 'src/volume-warning.vala'
2358--- src/volume-warning.vala 1970-01-01 00:00:00 +0000
2359+++ src/volume-warning.vala 2016-01-13 20:35:42 +0000
2360@@ -0,0 +1,216 @@
2361+/*
2362+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
2363+ *
2364+ * Copyright 2015 Canonical Ltd.
2365+ *
2366+ * This program is free software; you can redistribute it and/or modify
2367+ * it under the terms of the GNU General Public License as published by
2368+ * the Free Software Foundation; version 3.
2369+ *
2370+ * This program is distributed in the hope that it will be useful,
2371+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2372+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2373+ * GNU General Public License for more details.
2374+ *
2375+ * You should have received a copy of the GNU General Public License
2376+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2377+ *
2378+ * Authors:
2379+ * Charles Kerr <charles.kerr@canonical.com>
2380+ */
2381+
2382+using PulseAudio;
2383+
2384+public abstract class VolumeWarning : Object
2385+{
2386+ // true if headphones are in use
2387+ public bool headphones_active { get; set; default = false; }
2388+
2389+ // true if the warning dialog is being shown
2390+ public bool active { get; protected set; default = false; }
2391+
2392+ // true if we're playing unapproved loud multimedia over headphones
2393+ public bool high_volume { get; protected set; default = false; }
2394+
2395+ public enum Key {
2396+ VOLUME_UP,
2397+ VOLUME_DOWN
2398+ }
2399+
2400+ public void user_keypress (Key key) {
2401+ if ((key == Key.VOLUME_DOWN) && active) {
2402+ _notification.close ();
2403+ on_user_response (IndicatorSound.WarnNotification.Response.CANCEL);
2404+ }
2405+ }
2406+
2407+ public VolumeWarning (IndicatorSound.Options options) {
2408+
2409+ _options = options;
2410+
2411+ init_high_volume ();
2412+ init_approved ();
2413+
2414+ _notification.user_responded.connect ((n, r) => on_user_response (r));
2415+ }
2416+
2417+ ~VolumeWarning () {
2418+ clear_timer (ref _approved_timer);
2419+ }
2420+
2421+ /***
2422+ ****
2423+ ***/
2424+
2425+ // true if the user has approved high volumes recently
2426+ protected bool approved { get; set; default = false; }
2427+
2428+ // true if multimedia is currently playing
2429+ protected bool multimedia_active { get; set; default = false; }
2430+
2431+ /* Cached value of the multimedia volume reported by pulse.
2432+ Setting this only updates the cache -- to change the volume,
2433+ use sound_system_set_multimedia_volume.
2434+ NB: This PulseAudio.Volume is typed as uint to unconfuse valac. */
2435+ protected uint multimedia_volume { get; set; default = PulseAudio.Volume.INVALID; }
2436+
2437+ protected abstract void sound_system_set_multimedia_volume (PulseAudio.Volume volume);
2438+
2439+ protected void clear_timer (ref uint timer) {
2440+ if (timer != 0) {
2441+ Source.remove (timer);
2442+ timer = 0;
2443+ }
2444+ }
2445+
2446+ private IndicatorSound.Options _options;
2447+
2448+ /**
2449+ *** HIGH VOLUME PROPERTY
2450+ **/
2451+
2452+ private void init_high_volume () {
2453+ const string self_keys[] = {
2454+ "multimedia-volume",
2455+ "multimedia-active",
2456+ "headphones-active",
2457+ "high-volume-approved"
2458+ };
2459+ foreach (var key in self_keys)
2460+ this.notify[key].connect (() => update_high_volume ());
2461+
2462+ const string options_keys[] = {
2463+ "loud-volume",
2464+ "loud-warning-enabled"
2465+ };
2466+ foreach (var key in options_keys)
2467+ _options.notify[key].connect (() => update_high_volume ());
2468+
2469+ update_high_volume ();
2470+ }
2471+
2472+ private void update_high_volume () {
2473+
2474+ var newval = _options.loud_warning_enabled
2475+ && headphones_active
2476+ && multimedia_active
2477+ && !approved
2478+ && (multimedia_volume != PulseAudio.Volume.INVALID)
2479+ && (multimedia_volume >= _options.loud_volume);
2480+
2481+ if (high_volume != newval) {
2482+ debug ("changing high_volume from %d to %d", (int)high_volume, (int)newval);
2483+ if (newval && !active)
2484+ activate ();
2485+ high_volume = newval;
2486+ }
2487+ }
2488+
2489+ /**
2490+ *** HIGH VOLUME APPROVED PROPERTY
2491+ **/
2492+
2493+ private Settings _settings = new Settings ("com.canonical.indicator.sound");
2494+ private static const string TTL_KEY = "warning-volume-confirmation-ttl";
2495+ private uint _approved_timer = 0;
2496+ private int64 _approved_at = 0;
2497+ private int64 _approved_ttl_usec = 0;
2498+
2499+ private void approve_high_volume () {
2500+ _approved_at = GLib.get_monotonic_time ();
2501+ update_approved ();
2502+ update_approved_timer ();
2503+ }
2504+
2505+ private void init_approved () {
2506+ _settings.changed[TTL_KEY].connect (() => update_approved_cache ());
2507+ update_approved_cache ();
2508+ }
2509+ private void update_approved_cache () {
2510+ _approved_ttl_usec = _settings.get_int (TTL_KEY);
2511+ _approved_ttl_usec *= 1000000;
2512+
2513+ update_approved ();
2514+ update_approved_timer ();
2515+ }
2516+ private void update_approved_timer () {
2517+
2518+ clear_timer (ref _approved_timer);
2519+
2520+ if (_approved_at == 0)
2521+ return;
2522+
2523+ int64 expires_at = _approved_at + _approved_ttl_usec;
2524+ int64 now = GLib.get_monotonic_time ();
2525+ if (expires_at > now) {
2526+ var seconds_left = 1 + ((expires_at - now) / 1000000);
2527+ _approved_timer = Timeout.add_seconds ((uint)seconds_left, () => {
2528+ _approved_timer = 0;
2529+ update_approved ();
2530+ return Source.REMOVE;
2531+ });
2532+ }
2533+ }
2534+ private void update_approved () {
2535+ var new_approved = calculate_approved ();
2536+ if (approved != new_approved) {
2537+ debug ("changing approved from %d to %d", (int)approved, (int)new_approved);
2538+ approved = new_approved;
2539+ }
2540+ }
2541+ private bool calculate_approved () {
2542+ int64 now = GLib.get_monotonic_time ();
2543+ return (_approved_at != 0)
2544+ && (_approved_at + _approved_ttl_usec >= now);
2545+ }
2546+
2547+ // NOTIFICATION
2548+
2549+ private IndicatorSound.WarnNotification _notification = new IndicatorSound.WarnNotification ();
2550+ private PulseAudio.Volume _ok_volume = PulseAudio.Volume.INVALID;
2551+
2552+ protected virtual void preshow () {}
2553+
2554+ private void activate () {
2555+ preshow ();
2556+ _ok_volume = multimedia_volume;
2557+
2558+ _notification.show ();
2559+ this.active = true;
2560+
2561+ // lower the volume to just under the warning level
2562+ sound_system_set_multimedia_volume (_options.loud_volume-1);
2563+ }
2564+
2565+ private void on_user_response (IndicatorSound.WarnNotification.Response response) {
2566+
2567+ this.active = false;
2568+
2569+ if (response == IndicatorSound.WarnNotification.Response.OK) {
2570+ approve_high_volume ();
2571+ sound_system_set_multimedia_volume (_ok_volume);
2572+ }
2573+
2574+ _ok_volume = PulseAudio.Volume.INVALID;
2575+ }
2576+}
2577
2578=== added file 'src/warn-notification.vala'
2579--- src/warn-notification.vala 1970-01-01 00:00:00 +0000
2580+++ src/warn-notification.vala 2016-01-13 20:35:42 +0000
2581@@ -0,0 +1,59 @@
2582+/*
2583+ * Copyright 2015 Canonical Ltd.
2584+ *
2585+ * This program is free software; you can redistribute it and/or modify
2586+ * it under the terms of the GNU General Public License as published by
2587+ * the Free Software Foundation; version 3.
2588+ *
2589+ * This program is distributed in the hope that it will be useful,
2590+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2591+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2592+ * GNU General Public License for more details.
2593+ *
2594+ * You should have received a copy of the GNU General Public License
2595+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2596+ *
2597+ * Authors:
2598+ * Charles Kerr <charles.kerr@canonical.com>
2599+ */
2600+
2601+public class IndicatorSound.WarnNotification: Notification
2602+{
2603+ public enum Response {
2604+ CANCEL,
2605+ OK
2606+ }
2607+
2608+ public signal void user_responded (Response response);
2609+
2610+ protected override Notify.Notification create_notification () {
2611+ var n = new Notify.Notification (
2612+ _("Volume"),
2613+ _("High volume can damage your hearing."),
2614+ "audio-volume-high");
2615+ n.set_hint ("x-canonical-non-shaped-icon", "true");
2616+ n.set_hint ("x-canonical-snap-decisions", "true");
2617+ n.set_hint ("x-canonical-private-affirmative-tint", "true");
2618+ n.closed.connect ((n) => {
2619+ n.clear_actions ();
2620+ });
2621+ return n;
2622+ }
2623+
2624+ public bool show () {
2625+
2626+ if (!notify_server_supports ("actions"))
2627+ return false;
2628+
2629+ _notification.clear_actions ();
2630+ _notification.add_action ("ok", _("OK"), (n, a) => {
2631+ user_responded (Response.OK);
2632+ });
2633+ _notification.add_action ("cancel", _("Cancel"), (n, a) => {
2634+ user_responded (Response.CANCEL);
2635+ });
2636+ show_notification();
2637+
2638+ return true;
2639+ }
2640+}
2641
2642=== modified file 'tests/CMakeLists.txt'
2643--- tests/CMakeLists.txt 2016-01-05 15:08:24 +0000
2644+++ tests/CMakeLists.txt 2016-01-13 20:35:42 +0000
2645@@ -72,9 +72,17 @@
2646 )
2647
2648 vala_add(vala-mocks
2649+ options-mock.vala
2650+)
2651+
2652+vala_add(vala-mocks
2653 volume-control-mock.vala
2654 )
2655
2656+vala_add(vala-mocks
2657+ volume-warning-mock.vala
2658+)
2659+
2660 vala_finish(vala-mocks
2661 SOURCES
2662 vala_mocks_VALA_SOURCES
2663@@ -169,6 +177,7 @@
2664 target_link_libraries (
2665 volume-control-test
2666 indicator-sound-service-lib
2667+ vala-mocks-lib
2668 pulse-mock
2669 gtest-static
2670 ${TEST_LIBRARIES}
2671@@ -209,7 +218,7 @@
2672 ${TEST_LIBRARIES}
2673 )
2674
2675-#add_test(notifications-test notifications-test)
2676+add_test(notifications-test notifications-test)
2677
2678 ###########################
2679 # Accounts Service User
2680
2681=== modified file 'tests/integration/indicator-sound-test-base.cpp'
2682--- tests/integration/indicator-sound-test-base.cpp 2016-01-05 15:08:24 +0000
2683+++ tests/integration/indicator-sound-test-base.cpp 2016-01-13 20:35:42 +0000
2684@@ -163,13 +163,31 @@
2685 << "com.canonical.indicator.sound"
2686 << "interested-media-players"
2687 << "[]");
2688- if (!clearPlayers.waitForStarted())
2689- return false;
2690-
2691- if (!clearPlayers.waitForFinished())
2692- return false;
2693-
2694- return clearPlayers.exitCode() == 0;
2695+
2696+ return runProcess(clearPlayers);
2697+}
2698+
2699+bool IndicatorSoundTestBase::resetAllowAmplifiedVolume()
2700+{
2701+ QProcess proc;
2702+
2703+ proc.start("gsettings", QStringList()
2704+ << "reset"
2705+ << "com.ubuntu.sound"
2706+ << "allow-amplified-volume");
2707+
2708+ return runProcess(proc);
2709+}
2710+
2711+bool IndicatorSoundTestBase::runProcess(QProcess& proc)
2712+{
2713+ if (!proc.waitForStarted())
2714+ return false;
2715+
2716+ if (!proc.waitForFinished())
2717+ return false;
2718+
2719+ return proc.exitCode() == 0;
2720 }
2721
2722 bool IndicatorSoundTestBase::startTestMprisPlayer(QString const& playerName)
2723
2724=== modified file 'tests/integration/indicator-sound-test-base.h'
2725--- tests/integration/indicator-sound-test-base.h 2016-01-05 15:08:24 +0000
2726+++ tests/integration/indicator-sound-test-base.h 2016-01-13 20:35:42 +0000
2727@@ -77,6 +77,8 @@
2728 void startAccountsService();
2729
2730 bool clearGSettingsPlayers();
2731+ bool resetAllowAmplifiedVolume();
2732+ bool runProcess(QProcess&);
2733
2734 bool startTestMprisPlayer(QString const& playerName);
2735
2736
2737=== modified file 'tests/integration/test-indicator.cpp'
2738--- tests/integration/test-indicator.cpp 2016-01-05 15:08:24 +0000
2739+++ tests/integration/test-indicator.cpp 2016-01-13 20:35:42 +0000
2740@@ -32,7 +32,7 @@
2741 {
2742 };
2743
2744-TEST_F(TestIndicator, PhoneChangeRoleVolume)
2745+TEST_F(TestIndicator, DISABLED_PhoneChangeRoleVolume)
2746 {
2747 double INITIAL_VOLUME = 0.0;
2748
2749@@ -46,11 +46,11 @@
2750 // start now the indicator, so it picks the new volumes
2751 ASSERT_NO_THROW(startIndicator());
2752
2753- // Generate a random volume
2754+ // Generate a random volume in the range [0...0.33]
2755 QTime now = QTime::currentTime();
2756 qsrand(now.msec());
2757- int randInt = qrand() % 100;
2758- double randomVolume = randInt / 100.0;
2759+ int randInt = qrand() % 33;
2760+ const double randomVolume = randInt / 100.0;
2761
2762 QSignalSpy &userAccountsSpy = *signal_spy_volume_changed_;
2763 // set an initial volume to the alert role
2764@@ -542,6 +542,7 @@
2765 {
2766 double INITIAL_VOLUME = 0.0;
2767
2768+ EXPECT_TRUE(resetAllowAmplifiedVolume());
2769 ASSERT_NO_THROW(startAccountsService());
2770 ASSERT_NO_THROW(startPulseDesktop());
2771
2772@@ -556,11 +557,11 @@
2773 // start now the indicator, so it picks the new volumes
2774 ASSERT_NO_THROW(startIndicator());
2775
2776- // Generate a random volume
2777+ // Generate a random volume in the range [0...0.33]
2778 QTime now = QTime::currentTime();
2779 qsrand(now.msec());
2780- int randInt = qrand() % 100;
2781- double randomVolume = randInt / 100.0;
2782+ int randInt = qrand() % 33;
2783+ const double randomVolume = randInt / 100.0;
2784
2785 // play a test sound, it should NOT change the role in the indicator
2786 EXPECT_TRUE(startTestSound("multimedia"));
2787
2788=== modified file 'tests/notifications-mock.h'
2789--- tests/notifications-mock.h 2015-02-12 21:12:38 +0000
2790+++ tests/notifications-mock.h 2016-01-13 20:35:42 +0000
2791@@ -30,7 +30,7 @@
2792 DbusTestDbusMockObject * baseobj = nullptr;
2793
2794 public:
2795- 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"}) {
2796+ NotificationsMock (std::vector<std::string> capabilities = {"actions", "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"}) {
2797 mock = dbus_test_dbus_mock_new("org.freedesktop.Notifications");
2798 dbus_test_task_set_bus(DBUS_TEST_TASK(mock), DBUS_TEST_SERVICE_BUS_SESSION);
2799 dbus_test_task_set_name(DBUS_TEST_TASK(mock), "Notify");
2800
2801=== modified file 'tests/notifications-test.cc'
2802--- tests/notifications-test.cc 2015-12-23 13:22:35 +0000
2803+++ tests/notifications-test.cc 2016-01-13 20:35:42 +0000
2804@@ -1,5 +1,5 @@
2805 /*
2806- * Copyright © 2015 Canonical Ltd.
2807+ * Copyright © 2015-2016 Canonical Ltd.
2808 *
2809 * This program is free software; you can redistribute it and/or modify
2810 * it under the terms of the GNU General Public License as published by
2811@@ -15,8 +15,10 @@
2812 *
2813 * Authors:
2814 * Ted Gould <ted@canonical.com>
2815+ * Charles Kerr <charles.kerr@canonical.com>
2816 */
2817
2818+#include <algorithm>
2819 #include <memory>
2820
2821 #include <gtest/gtest.h>
2822@@ -105,6 +107,39 @@
2823 g_main_loop_unref(loop);
2824 }
2825
2826+ void loop_until(const std::function<bool()>& test, unsigned int max_ms=50, unsigned int test_interval_ms=10) {
2827+
2828+ // g_timeout's callback only allows a single pointer,
2829+ // so use a temporary stack struct to wedge everything into one pointer
2830+ struct CallbackData {
2831+ const std::function<bool()>& test;
2832+ const gint64 deadline;
2833+ GMainLoop* loop = g_main_loop_new(nullptr, false);
2834+ CallbackData (const std::function<bool()>& f, unsigned int max_ms):
2835+ test{f},
2836+ deadline{g_get_monotonic_time() + (max_ms*1000)} {}
2837+ ~CallbackData() {g_main_loop_unref(loop);}
2838+ } data(test, max_ms);
2839+
2840+ // tell the timer to stop looping on success or deadline
2841+ auto timerfunc = [](gpointer gdata) -> gboolean {
2842+ auto& data = *static_cast<CallbackData*>(gdata);
2843+ if (!data.test() && (g_get_monotonic_time() < data.deadline))
2844+ return G_SOURCE_CONTINUE;
2845+ g_main_loop_quit(data.loop);
2846+ return G_SOURCE_REMOVE;
2847+ };
2848+
2849+ // start looping
2850+ g_timeout_add (std::min(max_ms, test_interval_ms), timerfunc, &data);
2851+ g_main_loop_run(data.loop);
2852+ }
2853+
2854+ void loop_until_notifications(unsigned int max_seconds=1) {
2855+ auto test = [this]{ return !notifications->getNotifications().empty(); };
2856+ loop_until(test, max_seconds);
2857+ }
2858+
2859 static int unref_idle (gpointer user_data) {
2860 g_variant_unref(static_cast<GVariant *>(user_data));
2861 return G_SOURCE_REMOVE;
2862@@ -119,18 +154,40 @@
2863 return playerList;
2864 }
2865
2866- std::shared_ptr<VolumeControl> volumeControlMock () {
2867+ std::shared_ptr<IndicatorSoundOptions> optionsMock () {
2868+ auto options = std::shared_ptr<IndicatorSoundOptions>(
2869+ INDICATOR_SOUND_OPTIONS(options_mock_new()),
2870+ [](IndicatorSoundOptions * options){
2871+ g_clear_object(&options);
2872+ });
2873+ return options;
2874+ }
2875+
2876+ std::shared_ptr<VolumeControl> volumeControlMock (const std::shared_ptr<IndicatorSoundOptions>& optionsMock) {
2877 auto volumeControl = std::shared_ptr<VolumeControl>(
2878- VOLUME_CONTROL(volume_control_mock_new()),
2879+ VOLUME_CONTROL(volume_control_mock_new(optionsMock.get())),
2880 [](VolumeControl * control){
2881 g_clear_object(&control);
2882 });
2883 return volumeControl;
2884 }
2885
2886- std::shared_ptr<IndicatorSoundService> standardService (std::shared_ptr<VolumeControl> volumeControl, std::shared_ptr<MediaPlayerList> playerList) {
2887+ std::shared_ptr<VolumeWarning> volumeWarningMock (const std::shared_ptr<IndicatorSoundOptions>& optionsMock) {
2888+ auto volumeWarning = std::shared_ptr<VolumeWarning>(
2889+ VOLUME_WARNING(volume_warning_mock_new(optionsMock.get())),
2890+ [](VolumeWarning * warning){
2891+ g_clear_object(&warning);
2892+ });
2893+ return volumeWarning;
2894+ }
2895+
2896+ std::shared_ptr<IndicatorSoundService> standardService (
2897+ const std::shared_ptr<VolumeControl>& volumeControl,
2898+ const std::shared_ptr<MediaPlayerList>& playerList,
2899+ const std::shared_ptr<IndicatorSoundOptions>& options,
2900+ const std::shared_ptr<VolumeWarning>& warning) {
2901 auto soundService = std::shared_ptr<IndicatorSoundService>(
2902- indicator_sound_service_new(playerList.get(), volumeControl.get(), nullptr),
2903+ indicator_sound_service_new(playerList.get(), volumeControl.get(), nullptr, options.get(), warning.get()),
2904 [](IndicatorSoundService * service){
2905 g_clear_object(&service);
2906 });
2907@@ -165,10 +222,14 @@
2908
2909 g_clear_object(&bus);
2910 }
2911+
2912 };
2913
2914 TEST_F(NotificationsTest, BasicObject) {
2915- auto soundService = standardService(volumeControlMock(), playerListMock());
2916+ auto options = optionsMock();
2917+ auto volumeControl = volumeControlMock(options);
2918+ auto volumeWarning = volumeWarningMock(options);
2919+ auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning);
2920
2921 /* Give some time settle */
2922 loop(50);
2923@@ -177,8 +238,10 @@
2924 }
2925
2926 TEST_F(NotificationsTest, VolumeChanges) {
2927- auto volumeControl = volumeControlMock();
2928- auto soundService = standardService(volumeControl, playerListMock());
2929+ auto options = optionsMock();
2930+ auto volumeControl = volumeControlMock(options);
2931+ auto volumeWarning = volumeWarningMock(options);
2932+ auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning);
2933
2934 /* Set a volume */
2935 notifications->clearNotifications();
2936@@ -216,8 +279,10 @@
2937 }
2938
2939 TEST_F(NotificationsTest, StreamChanges) {
2940- auto volumeControl = volumeControlMock();
2941- auto soundService = standardService(volumeControl, playerListMock());
2942+ auto options = optionsMock();
2943+ auto volumeControl = volumeControlMock(options);
2944+ auto volumeWarning = volumeWarningMock(options);
2945+ auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning);
2946
2947 /* Set a volume */
2948 notifications->clearNotifications();
2949@@ -228,7 +293,7 @@
2950
2951 /* Change Streams, no volume change */
2952 notifications->clearNotifications();
2953- volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "alarm");
2954+ volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_ALARM);
2955 setMockVolume(volumeControl, 0.5, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE);
2956 loop(50);
2957 notev = notifications->getNotifications();
2958@@ -236,7 +301,7 @@
2959
2960 /* Change Streams, volume change */
2961 notifications->clearNotifications();
2962- volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "alert");
2963+ volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_ALERT);
2964 setMockVolume(volumeControl, 0.6, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE);
2965 loop(50);
2966 notev = notifications->getNotifications();
2967@@ -244,7 +309,7 @@
2968
2969 /* Change Streams, no volume change, volume up */
2970 notifications->clearNotifications();
2971- volume_control_mock_set_mock_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), "multimedia");
2972+ volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_MULTIMEDIA);
2973 setMockVolume(volumeControl, 0.6, VOLUME_CONTROL_VOLUME_REASONS_VOLUME_STREAM_CHANGE);
2974 loop(50);
2975 setMockVolume(volumeControl, 0.65);
2976@@ -254,8 +319,10 @@
2977 }
2978
2979 TEST_F(NotificationsTest, IconTesting) {
2980- auto volumeControl = volumeControlMock();
2981- auto soundService = standardService(volumeControl, playerListMock());
2982+ auto options = optionsMock();
2983+ auto volumeControl = volumeControlMock(options);
2984+ auto volumeWarning = volumeWarningMock(options);
2985+ auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning);
2986
2987 /* Set an initial volume */
2988 notifications->clearNotifications();
2989@@ -288,8 +355,10 @@
2990 }
2991
2992 TEST_F(NotificationsTest, ServerRestart) {
2993- auto volumeControl = volumeControlMock();
2994- auto soundService = standardService(volumeControl, playerListMock());
2995+ auto options = optionsMock();
2996+ auto volumeControl = volumeControlMock(options);
2997+ auto volumeWarning = volumeWarningMock(options);
2998+ auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning);
2999
3000 /* Set a volume */
3001 notifications->clearNotifications();
3002@@ -335,8 +404,10 @@
3003 }
3004
3005 TEST_F(NotificationsTest, HighVolume) {
3006- auto volumeControl = volumeControlMock();
3007- auto soundService = standardService(volumeControl, playerListMock());
3008+ auto options = optionsMock();
3009+ auto volumeControl = volumeControlMock(options);
3010+ auto volumeWarning = volumeWarningMock(options);
3011+ auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning);
3012
3013 /* Set a volume */
3014 notifications->clearNotifications();
3015@@ -350,7 +421,7 @@
3016
3017 /* Set high volume with volume change */
3018 notifications->clearNotifications();
3019- volume_control_mock_set_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), true);
3020+ volume_warning_mock_set_high_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), true);
3021 setMockVolume(volumeControl, 0.90);
3022 loop(50);
3023 notev = notifications->getNotifications();
3024@@ -360,14 +431,14 @@
3025 EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-value-bar-tint"]);
3026
3027 /* Move it back */
3028- volume_control_mock_set_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), false);
3029+ volume_warning_mock_set_high_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), false);
3030 setMockVolume(volumeControl, 0.50);
3031 loop(50);
3032
3033 /* Set high volume without level change */
3034 /* NOTE: This can happen if headphones are plugged in */
3035 notifications->clearNotifications();
3036- volume_control_mock_set_high_volume(VOLUME_CONTROL_MOCK(volumeControl.get()), TRUE);
3037+ volume_warning_mock_set_high_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), true);
3038 loop(50);
3039 notev = notifications->getNotifications();
3040 ASSERT_EQ(1, notev.size());
3041@@ -377,8 +448,10 @@
3042 }
3043
3044 TEST_F(NotificationsTest, MenuHide) {
3045- auto volumeControl = volumeControlMock();
3046- auto soundService = standardService(volumeControl, playerListMock());
3047+ auto options = optionsMock();
3048+ auto volumeControl = volumeControlMock(options);
3049+ auto volumeWarning = volumeWarningMock(options);
3050+ auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning);
3051
3052 /* Set a volume */
3053 notifications->clearNotifications();
3054@@ -406,9 +479,11 @@
3055 EXPECT_EQ(1, notev.size());
3056 }
3057
3058-TEST_F(NotificationsTest, DISABLED_ExtendendVolumeNotification) {
3059- auto volumeControl = volumeControlMock();
3060- auto soundService = standardService(volumeControl, playerListMock());
3061+TEST_F(NotificationsTest, ExtendendVolumeNotification) {
3062+ auto options = optionsMock();
3063+ auto volumeControl = volumeControlMock(options);
3064+ auto volumeWarning = volumeWarningMock(options);
3065+ auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning);
3066
3067 /* Set a volume */
3068 notifications->clearNotifications();
3069@@ -424,7 +499,8 @@
3070
3071 /* Allow an amplified volume */
3072 notifications->clearNotifications();
3073- //indicator_sound_service_set_allow_amplified_volume(soundService.get(), TRUE);
3074+ volume_control_mock_mock_set_active_stream(VOLUME_CONTROL_MOCK(volumeControl.get()), VOLUME_CONTROL_STREAM_ALARM);
3075+ options_mock_mock_set_max_volume(OPTIONS_MOCK(options.get()), 1.5);
3076 loop(50);
3077 notev = notifications->getNotifications();
3078 ASSERT_EQ(1, notev.size());
3079@@ -440,9 +516,112 @@
3080
3081 /* Put back */
3082 notifications->clearNotifications();
3083- //indicator_sound_service_set_allow_amplified_volume(soundService.get(), FALSE);
3084+ options_mock_mock_set_max_volume(OPTIONS_MOCK(options.get()), 1.0);
3085 loop(50);
3086 notev = notifications->getNotifications();
3087 ASSERT_EQ(1, notev.size());
3088 EXPECT_GVARIANT_EQ("@i 100", notev[0].hints["value"]);
3089 }
3090+
3091+TEST_F(NotificationsTest, TriggerWarning) {
3092+
3093+ // Tests all the conditions needed to trigger a volume warning.
3094+ // There are many possible combinations, so this test is slow. :P
3095+
3096+ const struct {
3097+ bool expected;
3098+ VolumeControlActiveOutput output;
3099+ } test_outputs[] = {
3100+ { false, VOLUME_CONTROL_ACTIVE_OUTPUT_SPEAKERS },
3101+ { true, VOLUME_CONTROL_ACTIVE_OUTPUT_HEADPHONES },
3102+ { true, VOLUME_CONTROL_ACTIVE_OUTPUT_BLUETOOTH_HEADPHONES },
3103+ { false, VOLUME_CONTROL_ACTIVE_OUTPUT_BLUETOOTH_SPEAKER },
3104+ { false, VOLUME_CONTROL_ACTIVE_OUTPUT_USB_SPEAKER },
3105+ { true, VOLUME_CONTROL_ACTIVE_OUTPUT_USB_HEADPHONES },
3106+ { false, VOLUME_CONTROL_ACTIVE_OUTPUT_HDMI_SPEAKER },
3107+ { true, VOLUME_CONTROL_ACTIVE_OUTPUT_HDMI_HEADPHONES },
3108+ { false, VOLUME_CONTROL_ACTIVE_OUTPUT_CALL_MODE }
3109+ };
3110+
3111+ const struct {
3112+ bool expected;
3113+ pa_volume_t volume;
3114+ pa_volume_t loud_volume;
3115+ } test_volumes[] = {
3116+ { false, 50, 100 },
3117+ { false, 99, 100 },
3118+ { true, 100, 100 },
3119+ { true, 101, 100 }
3120+ };
3121+
3122+ const struct {
3123+ bool expected;
3124+ bool approved;
3125+ } test_approved[] = {
3126+ { true, false },
3127+ { false, true }
3128+ };
3129+
3130+ const struct {
3131+ bool expected;
3132+ bool warnings_enabled;
3133+ } test_warnings_enabled[] = {
3134+ { true, true },
3135+ { false, false }
3136+ };
3137+
3138+ const struct {
3139+ bool expected;
3140+ bool multimedia_active;
3141+ } test_multimedia_active[] = {
3142+ { true, true },
3143+ { false, false }
3144+ };
3145+
3146+ for (const auto& outputs : test_outputs) {
3147+ for (const auto& volumes : test_volumes) {
3148+ for (const auto& approved : test_approved) {
3149+ for (const auto& warnings_enabled : test_warnings_enabled) {
3150+ for (const auto& multimedia_active : test_multimedia_active) {
3151+
3152+ notifications->clearNotifications();
3153+
3154+ // instantiate the test subjects
3155+ auto options = optionsMock();
3156+ auto volumeControl = volumeControlMock(options);
3157+ auto volumeWarning = volumeWarningMock(options);
3158+ auto soundService = standardService(volumeControl, playerListMock(), options, volumeWarning);
3159+
3160+ // run the test
3161+ options_mock_mock_set_loud_volume(OPTIONS_MOCK(options.get()), volumes.loud_volume);
3162+ options_mock_mock_set_loud_warning_enabled(OPTIONS_MOCK(options.get()), warnings_enabled.warnings_enabled);
3163+ volume_warning_mock_set_approved(VOLUME_WARNING_MOCK(volumeWarning.get()), approved.approved);
3164+ volume_warning_mock_set_multimedia_volume(VOLUME_WARNING_MOCK(volumeWarning.get()), volumes.volume);
3165+ volume_warning_mock_set_multimedia_active(VOLUME_WARNING_MOCK(volumeWarning.get()), multimedia_active.multimedia_active);
3166+ volume_control_mock_mock_set_active_output(VOLUME_CONTROL_MOCK(volumeControl.get()), outputs.output);
3167+
3168+ loop_until_notifications();
3169+
3170+ // check the result
3171+ auto notev = notifications->getNotifications();
3172+ const bool warning_expected = outputs.expected && volumes.expected && approved.expected && warnings_enabled.expected && multimedia_active.expected;
3173+ if (warning_expected) {
3174+ EXPECT_TRUE(volume_warning_get_active(volumeWarning.get()));
3175+ ASSERT_EQ(1, notev.size());
3176+ EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-snap-decisions"]);
3177+ EXPECT_GVARIANT_EQ(nullptr, notev[0].hints["x-canonical-private-synchronous"]);
3178+ }
3179+ else {
3180+ EXPECT_FALSE(volume_warning_get_active(volumeWarning.get()));
3181+ ASSERT_EQ(1, notev.size());
3182+ EXPECT_GVARIANT_EQ(nullptr, notev[0].hints["x-canonical-snap-decisions"]);
3183+ EXPECT_GVARIANT_EQ("@s 'true'", notev[0].hints["x-canonical-private-synchronous"]);
3184+ }
3185+
3186+ } // multimedia_active
3187+ } // warnings_enabled
3188+ } // approved
3189+ } // volumes
3190+ } // outputs
3191+}
3192+
3193
3194=== added file 'tests/options-mock.vala'
3195--- tests/options-mock.vala 1970-01-01 00:00:00 +0000
3196+++ tests/options-mock.vala 2016-01-13 20:35:42 +0000
3197@@ -0,0 +1,28 @@
3198+/*
3199+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
3200+ * Copyright © 2015 Canonical Ltd.
3201+ *
3202+ * This program is free software; you can redistribute it and/or modify
3203+ * it under the terms of the GNU General Public License as published by
3204+ * the Free Software Foundation; version 3.
3205+ *
3206+ * This program is distributed in the hope that it will be useful,
3207+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3208+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3209+ * GNU General Public License for more details.
3210+ *
3211+ * You should have received a copy of the GNU General Public License
3212+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
3213+ *
3214+ * Authors:
3215+ * Charles Kerr <charles.kerr@canonical.com>
3216+ */
3217+
3218+public class OptionsMock : IndicatorSound.Options
3219+{
3220+ public void mock_set_max_volume (double val) { max_volume = val; }
3221+
3222+ public void mock_set_loud_volume (PulseAudio.Volume val) { loud_volume = val; }
3223+
3224+ public void mock_set_loud_warning_enabled (bool val) { loud_warning_enabled = val; }
3225+}
3226
3227=== modified file 'tests/pa-mock.cpp'
3228--- tests/pa-mock.cpp 2015-01-29 14:34:50 +0000
3229+++ tests/pa-mock.cpp 2016-01-13 20:35:42 +0000
3230@@ -351,6 +351,25 @@
3231 return dummy_operation();
3232 }
3233
3234+pa_operation*
3235+pa_context_get_sink_input_info_list(pa_context *c, pa_sink_input_info_cb_t cb, void *userdata)
3236+{
3237+ reinterpret_cast<PAMockContext*>(c)->idleOnce(
3238+ [c, cb, userdata]() {
3239+
3240+ pa_sink_input_info sink_input;
3241+ sink_input.name = "default-sink-input";
3242+ sink_input.proplist = nullptr;
3243+ sink_input.has_volume = false;
3244+
3245+ if (cb != nullptr)
3246+ cb(c, &sink_input, true, userdata);
3247+ });
3248+
3249+ return dummy_operation();
3250+}
3251+
3252+
3253 /* *******************************
3254 * subscribe.h
3255 * *******************************/
3256
3257=== modified file 'tests/volume-control-mock.vala'
3258--- tests/volume-control-mock.vala 2015-08-11 22:19:05 +0000
3259+++ tests/volume-control-mock.vala 2016-01-13 20:35:42 +0000
3260@@ -20,27 +20,33 @@
3261
3262 public class VolumeControlMock : VolumeControl
3263 {
3264- private bool _high_volume = false;
3265- public override bool high_volume { get { return _high_volume; } protected set { _high_volume = value; } }
3266- public void set_high_volume(bool b) { high_volume = b; }
3267-
3268- public string mock_stream { get; set; default = "multimedia"; }
3269- public override string stream { get { return mock_stream; } }
3270- public override bool ready { get; set; }
3271+ public void mock_set_is_ready(bool b) { ready = b; }
3272+ public void mock_set_active_stream(VolumeControl.Stream s) { active_stream = s; }
3273+ public void mock_set_is_playing(bool b) { is_playing = b; }
3274 public override bool active_mic { get; set; }
3275 public bool mock_mute { get; set; }
3276 public override bool mute { get { return mock_mute; } }
3277- public bool mock_is_playing { get; set; }
3278- public override bool is_playing { get { return mock_is_playing; } }
3279 private VolumeControl.Volume _vol = new VolumeControl.Volume();
3280 public override VolumeControl.Volume volume { get { return _vol; } set { _vol = value; }}
3281 public override double mic_volume { get; set; }
3282
3283 public override void set_mute (bool mute) {
3284-
3285- }
3286-
3287- public VolumeControlMock() {
3288+ }
3289+
3290+ private VolumeControl.ActiveOutput _active_output = VolumeControl.ActiveOutput.SPEAKERS;
3291+
3292+ public override VolumeControl.ActiveOutput active_output() {
3293+ return _active_output;
3294+ }
3295+
3296+ public void mock_set_active_output (VolumeControl.ActiveOutput val) {
3297+ _active_output = val;
3298+ this.active_output_changed(val);
3299+ }
3300+
3301+ public VolumeControlMock(IndicatorSound.Options options) {
3302+ base(options);
3303+
3304 ready = true;
3305 this.notify["mock-stream"].connect(() => this.notify_property("stream"));
3306 this.notify["mock-high-volume"].connect(() => this.notify_property("high-volume"));
3307
3308=== modified file 'tests/volume-control-test.cc'
3309--- tests/volume-control-test.cc 2015-08-07 19:28:41 +0000
3310+++ tests/volume-control-test.cc 2016-01-13 20:35:42 +0000
3311@@ -23,6 +23,7 @@
3312
3313 extern "C" {
3314 #include "indicator-sound-service.h"
3315+#include "vala-mocks.h"
3316 }
3317
3318 class VolumeControlTest : public ::testing::Test
3319@@ -75,7 +76,9 @@
3320 };
3321
3322 TEST_F(VolumeControlTest, BasicObject) {
3323- VolumeControlPulse * control = volume_control_pulse_new();
3324+ auto options = options_mock_new();
3325+ auto pgloop = pa_glib_mainloop_new(NULL);
3326+ auto control = volume_control_pulse_new(INDICATOR_SOUND_OPTIONS(options), pgloop);
3327
3328 /* Setup the PA backend */
3329 loop(100);
3330@@ -84,4 +87,6 @@
3331 EXPECT_TRUE(volume_control_get_ready(VOLUME_CONTROL(control)));
3332
3333 g_clear_object(&control);
3334+ g_clear_object(&options);
3335+ g_clear_pointer(&pgloop, pa_glib_mainloop_free);
3336 }
3337
3338=== added file 'tests/volume-warning-mock.vala'
3339--- tests/volume-warning-mock.vala 1970-01-01 00:00:00 +0000
3340+++ tests/volume-warning-mock.vala 2016-01-13 20:35:42 +0000
3341@@ -0,0 +1,40 @@
3342+/*
3343+ * -*- Mode:Vala; indent-tabs-mode:t; tab-width:4; encoding:utf8 -*-
3344+ * Copyright © 2015 Canonical Ltd.
3345+ *
3346+ * This program is free software; you can redistribute it and/or modify
3347+ * it under the terms of the GNU General Public License as published by
3348+ * the Free Software Foundation; version 3.
3349+ *
3350+ * This program is distributed in the hope that it will be useful,
3351+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3352+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3353+ * GNU General Public License for more details.
3354+ *
3355+ * You should have received a copy of the GNU General Public License
3356+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
3357+ *
3358+ * Authors:
3359+ * Ted Gould <ted@canonical.com>
3360+ */
3361+
3362+public class VolumeWarningMock : VolumeWarning
3363+{
3364+ public void set_high_volume(bool val) { high_volume = val; }
3365+
3366+ public VolumeWarningMock(IndicatorSound.Options options) {
3367+ base(options);
3368+ }
3369+
3370+ protected override void sound_system_set_multimedia_volume(PulseAudio.Volume volume) {
3371+ GLib.message("volume-warning-mock setting multimedia volume to %d", (int)volume);
3372+ }
3373+
3374+ public void set_multimedia_active(bool val) { multimedia_active = val; }
3375+
3376+ public void set_multimedia_volume(PulseAudio.Volume val) { multimedia_volume = val; }
3377+
3378+ public void set_approved(bool val) { approved = val; }
3379+
3380+ public bool is_approved() { return approved; }
3381+}

Subscribers

People subscribed via source and target branches