aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bzrignore10
-rw-r--r--CMakeLists.txt8
-rw-r--r--MERGE-REVIEW19
-rw-r--r--data/50-com.canonical.indicator.sound.AccountsService.pkla6
-rw-r--r--data/CMakeLists.txt32
-rw-r--r--data/com.canonical.indicator.sound3
-rw-r--r--data/com.canonical.indicator.sound.AccountsService.policy23
-rw-r--r--data/com.canonical.indicator.sound.AccountsService.xml42
-rw-r--r--data/com.canonical.indicator.sound.gschema.xml9
-rw-r--r--debian/changelog94
-rw-r--r--debian/control9
-rwxr-xr-xdebian/rules5
-rw-r--r--src/CMakeLists.txt61
-rw-r--r--src/accounts-service-sound-settings.vala33
-rw-r--r--src/accounts-service-user.vala135
-rw-r--r--src/main.c32
-rw-r--r--src/main.vala14
-rw-r--r--src/media-player-list.vala20
-rw-r--r--src/media-player-mpris.vala299
-rw-r--r--src/media-player.vala297
-rw-r--r--src/service.vala185
-rw-r--r--src/sound-menu.vala11
-rw-r--r--src/volume-control.vala17
-rw-r--r--tests/CMakeLists.txt94
-rw-r--r--tests/accounts-service-mock.h90
-rw-r--r--tests/accounts-service-user.cc201
-rw-r--r--tests/manual24
-rw-r--r--tests/media-player-mock.vala76
28 files changed, 1521 insertions, 328 deletions
diff --git a/.bzrignore b/.bzrignore
index 166a1ca..d010156 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -13,3 +13,13 @@
/src/service.c
/src/volume-control.c
/src/freedesktop-interfaces.c
+
+# CMake Generated Files
+CMakeFiles/
+Makefile
+cmake_install.cmake
+CMakeCache.txt
+CTestTestfile.cmake
+config.h
+
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ecd2f12..8c8d69c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -40,11 +40,19 @@ pkg_check_modules(
gee-1.0
gio-2.0>=${GIO_2_0_REQUIRED_VERSION}
gio-unix-2.0
+ gthread-2.0
libxml-2.0
libnotify
+ accountsservice
)
include_directories(${SOUNDSERVICE_INCLUDE_DIRS})
+pkg_check_modules(
+ TEST REQUIRED
+ dbustest-1
+)
+include_directories(${TEST_INCLUDE_DIRS})
+
find_package(Vala 0.20)
find_package(GObjectIntrospection 0.9.12)
diff --git a/MERGE-REVIEW b/MERGE-REVIEW
new file mode 100644
index 0000000..1b7ff95
--- /dev/null
+++ b/MERGE-REVIEW
@@ -0,0 +1,19 @@
+
+This documents the expections that the project has on what both submitters
+and reviewers should ensure that they've done for a merge into the project.
+
+== Submitter Responsibilities ==
+
+ * Ensure the project compiles and the test suite executes without error
+ * Ensure that non-obvious code has comments explaining it
+ * If the change works on specific profiles, please include those in the merge description.
+
+== Reviewer Responsibilities ==
+
+ * Did the Jenkins build compile? Pass? Run unit tests successfully?
+ * Are there appropriate tests to cover any new functionality?
+ * If the description says this effects the phone profile:
+ * Run tests indicator-sound/unity8*
+ * If the description says this effects the desktop profile:
+ * Run tests indicator-sound/unity7*
+
diff --git a/data/50-com.canonical.indicator.sound.AccountsService.pkla b/data/50-com.canonical.indicator.sound.AccountsService.pkla
new file mode 100644
index 0000000..bbcca1e
--- /dev/null
+++ b/data/50-com.canonical.indicator.sound.AccountsService.pkla
@@ -0,0 +1,6 @@
+[Allow LightDM to set Unity AccountsService fields]
+Identity=unix-user:lightdm
+Action=com.canonical.indicator.sound.AccountsService.ModifyAnyUser
+ResultActive=yes
+ResultInactive=yes
+ResultAny=yes
diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt
index 565e652..9694a9e 100644
--- a/data/CMakeLists.txt
+++ b/data/CMakeLists.txt
@@ -61,3 +61,35 @@ install(
###########################
add_schema ("com.canonical.indicator.sound.gschema.xml")
+
+###########################
+# Accounts Service
+###########################
+
+
+set(POLKIT_LIB_DIR "${CMAKE_INSTALL_LOCALSTATEDIR}/lib/polkit-1")
+set(POLKIT_DATA_DIR "${CMAKE_INSTALL_PREFIX}/share/polkit-1")
+set(DBUS_IFACE_DIR "${CMAKE_INSTALL_PREFIX}/share/dbus-1/interfaces")
+set(ACCOUNTS_IFACE_DIR "${CMAKE_INSTALL_PREFIX}/share/accountsservice/interfaces")
+
+install(FILES com.canonical.indicator.sound.AccountsService.xml
+ DESTINATION "${DBUS_IFACE_DIR}"
+)
+
+# Create accountsservice symlink for above dbus interface
+install(CODE "
+ execute_process(COMMAND mkdir -p \"\$ENV{DESTDIR}${ACCOUNTS_IFACE_DIR}\")
+ execute_process(COMMAND ln -sf ../../dbus-1/interfaces/com.canonical.indicator.sound.AccountsService.xml \"\$ENV{DESTDIR}${ACCOUNTS_IFACE_DIR}\")
+")
+
+install(FILES com.canonical.indicator.sound.AccountsService.policy
+DESTINATION "${POLKIT_DATA_DIR}/actions"
+)
+
+install(FILES 50-com.canonical.indicator.sound.AccountsService.pkla
+DESTINATION "${POLKIT_LIB_DIR}/localauthority/10-vendor.d"
+)
+
+
+
+
diff --git a/data/com.canonical.indicator.sound b/data/com.canonical.indicator.sound
index ae5fd5d..fca15be 100644
--- a/data/com.canonical.indicator.sound
+++ b/data/com.canonical.indicator.sound
@@ -12,6 +12,9 @@ ObjectPath=/com/canonical/indicator/sound/phone
[desktop_greeter]
ObjectPath=/com/canonical/indicator/sound/desktop_greeter
+[desktop_lockscreen]
+ObjectPath=/com/canonical/indicator/sound/desktop_greeter
+
[ubiquity]
ObjectPath=/com/canonical/indicator/sound/desktop_greeter
diff --git a/data/com.canonical.indicator.sound.AccountsService.policy b/data/com.canonical.indicator.sound.AccountsService.policy
new file mode 100644
index 0000000..4d0ee75
--- /dev/null
+++ b/data/com.canonical.indicator.sound.AccountsService.policy
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<policyconfig>
+ <action id="com.canonical.indicator.sound.AccountsService.ModifyOwnUser">
+ <description>Set properties of own user</description>
+ <message>Authentication is required to set one's own indicator sound properties.</message>
+ <defaults>
+ <allow_any>yes</allow_any>
+ <allow_inactive>yes</allow_inactive>
+ <allow_active>yes</allow_active>
+ </defaults>
+ </action>
+
+ <action id="com.canonical.indicator.sound.AccountsService.ModifyAnyUser">
+ <description>Set properties of any user</description>
+ <message>Authentication is required to set another user's indicator sound properties.</message>
+ <defaults>
+ <allow_any>no</allow_any>
+ <allow_inactive>no</allow_inactive>
+ <allow_active>no</allow_active>
+ </defaults>
+ </action>
+</policyconfig>
diff --git a/data/com.canonical.indicator.sound.AccountsService.xml b/data/com.canonical.indicator.sound.AccountsService.xml
new file mode 100644
index 0000000..fb7e96f
--- /dev/null
+++ b/data/com.canonical.indicator.sound.AccountsService.xml
@@ -0,0 +1,42 @@
+<node>
+ <interface name="com.canonical.indicator.sound.AccountsService">
+
+ <annotation name="org.freedesktop.Accounts.VendorExtension" value="true"/>
+
+ <annotation name="org.freedesktop.Accounts.Authentication.ChangeOwn"
+ value="com.canonical.indicator.sound.AccountsService.ModifyOwnUser"/>
+
+ <annotation name="org.freedesktop.Accounts.Authentication.ReadAny"
+ value="com.canonical.indicator.sound.AccountsService.ModifyAnyUser"/>
+
+ <annotation name="org.freedesktop.Accounts.Authentication.ChangeAny"
+ value="com.canonical.indicator.sound.AccountsService.ModifyAnyUser"/>
+
+ <property name="Timestamp" type="t" access="readwrite">
+ <annotation name="org.freedesktop.Accounts.DefaultValue" value="0"/>
+ </property>
+ <property name="PlayerName" type="s" access="readwrite">
+ <annotation name="org.freedesktop.Accounts.DefaultValue" value=""/>
+ </property>
+ <property name="PlayerIcon" type="v" access="readwrite"/>
+ <property name="Running" type="b" access="readwrite">
+ <annotation name="org.freedesktop.Accounts.DefaultValue" value="false"/>
+ </property>
+ <property name="State" type="s" access="readwrite">
+ <annotation name="org.freedesktop.Accounts.DefaultValue" value=""/>
+ </property>
+ <property name="Title" type="s" access="readwrite">
+ <annotation name="org.freedesktop.Accounts.DefaultValue" value=""/>
+ </property>
+ <property name="Artist" type="s" access="readwrite">
+ <annotation name="org.freedesktop.Accounts.DefaultValue" value=""/>
+ </property>
+ <property name="Album" type="s" access="readwrite">
+ <annotation name="org.freedesktop.Accounts.DefaultValue" value=""/>
+ </property>
+ <property name="ArtUrl" type="s" access="readwrite">
+ <annotation name="org.freedesktop.Accounts.DefaultValue" value=""/>
+ </property>
+
+ </interface>
+</node>
diff --git a/data/com.canonical.indicator.sound.gschema.xml b/data/com.canonical.indicator.sound.gschema.xml
index 102a1db..c34dfd6 100644
--- a/data/com.canonical.indicator.sound.gschema.xml
+++ b/data/com.canonical.indicator.sound.gschema.xml
@@ -48,5 +48,14 @@
Whether or not to show the sound indicator in the menu bar.
</description>
</key>
+ <key name="greeter-export" type="b">
+ <default>true</default>
+ <summary>Whether or not to export the currently playing song to the greeter.</summary>
+ <description>
+ If enabled the sound indicator will export the current player and
+ song to the greeter so that it can be shown if the user is selected
+ and the sound menu is shown.
+ </description>
+ </key>
</schema>
</schemalist>
diff --git a/debian/changelog b/debian/changelog
index 2599770..d878559 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,97 @@
+indicator-sound (12.10.2+14.04.20140320-0ubuntu1) trusty; urgency=low
+
+ [ Lars Uebernickel ]
+ * Scale volume in notifications when allow-amplified-volume is set
+ (LP: #1293163)
+ * Use audio-volume-mute-blocking-panel instead of *-blocked-panel
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Thu, 20 Mar 2014 08:58:30 +0000
+
+indicator-sound (12.10.2+14.04.20140318-0ubuntu1) trusty; urgency=low
+
+ [ Lars Uebernickel ]
+ * Show a red icon in the panel when a sound is playing while mute is
+ on (LP: #1291530)
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Tue, 18 Mar 2014 10:01:01 +0000
+
+indicator-sound (12.10.2+14.04.20140313-0ubuntu1) trusty; urgency=low
+
+ [ Lars Uebernickel ]
+ * Don't show player sections on the greeter and lock screen (LP:
+ #1280378)
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Thu, 13 Mar 2014 15:33:11 +0000
+
+indicator-sound (12.10.2+14.04.20140311.1-0ubuntu1) trusty; urgency=low
+
+ [ Lars Uebernickel ]
+ * Add desktop_lockscreen profile
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Tue, 11 Mar 2014 18:26:29 +0000
+
+indicator-sound (12.10.2+14.04.20140305-0ubuntu1) trusty; urgency=low
+
+ [ Lars Uebernickel ]
+ * Honor com.canonical.indicator.sound visible (LP: #829648)
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Wed, 05 Mar 2014 08:44:14 +0000
+
+indicator-sound (12.10.2+14.04.20140224-0ubuntu1) trusty; urgency=low
+
+ [ Sebastien Bacher ]
+ * Remove the new added allow-applified key, it's moved to shared
+ schemas, that way the other desktop components don't need to depends
+ on indicator-sound only to read that settings
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Mon, 24 Feb 2014 14:33:07 +0000
+
+indicator-sound (12.10.2+14.04.20140220-0ubuntu1) trusty; urgency=low
+
+ [ Lars Uebernickel ]
+ * Add support for amplified volumes Add a settings key "allow-
+ amplified-volume" which controls whether the volume slider stops at
+ 100% or PA_VOLUME_UI_MAX. unity-control-center will provide a ui for
+ this key.
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Thu, 20 Feb 2014 18:13:42 +0000
+
+indicator-sound (12.10.2+14.04.20140207-0ubuntu1) trusty; urgency=low
+
+ [ Unit193 ]
+ * Drop the recommends to suggests for the control centers and system
+ settings so that flavors that don't seed those packages aren't stuck
+ with them.
+
+ [ Ted Gould ]
+ * Adding acceptance tests and merge review policies.
+
+ [ CI bot ]
+ * Adding acceptance tests and merge review policies
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Fri, 07 Feb 2014 16:26:07 +0000
+
+indicator-sound (12.10.2+14.04.20140124-0ubuntu1) trusty; urgency=low
+
+ [ Robert Ancell ]
+ * Use unity-control-center if it is available. (LP: #1257505)
+
+ [ Ted Gould ]
+ * Own the server capabilities so we clean it up.
+
+ [ Lars Uebernickel ]
+ * Don't write 'interested-media-players' on startup
+ IndicatorSound.Service read that gsettings key and inserted the
+ players desktop ids into a MediaPlayerList, which emits "player-
+ added" every time a player is added. This patch makes the service
+ keep track of whether players are added because it is syncing the
+ key or when a player appeared on the bus. .
+
+ [ Ubuntu daily release ]
+ * Automatic snapshot from revision 408
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Fri, 24 Jan 2014 11:00:18 +0000
+
indicator-sound (12.10.2+14.04.20131125-0ubuntu1) trusty; urgency=low
[ Ted Gould ]
diff --git a/debian/control b/debian/control
index ab0c0f1..4278f7a 100644
--- a/debian/control
+++ b/debian/control
@@ -6,10 +6,15 @@ XSBC-Original-Maintainer: Conor Curran <conor.curran@canonical.com>
Build-Depends: debhelper (>= 9.0),
cmake,
dbus,
+ dbus-test-runner (>= 14.04.0+14.04.20140226),
dh-translations,
+ gir1.2-accountsservice-1.0,
gnome-common,
autotools-dev,
valac (>= 0.20),
+ libaccountsservice-dev,
+ libdbustest1-dev,
+ libgirepository1.0-dev,
libglib2.0-dev (>= 2.22.3),
libgtest-dev,
liburl-dispatcher1-dev,
@@ -18,6 +23,7 @@ Build-Depends: debhelper (>= 9.0),
libnotify-dev,
libgee-dev,
libxml2-dev,
+ python3-dbusmock,
Standards-Version: 3.9.4
Homepage: https://launchpad.net/indicator-sound
# If you aren't a member of ~indicator-applet-developers but need to upload
@@ -31,7 +37,8 @@ Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
pulseaudio,
-Recommends: gnome-control-center | ubuntu-system-settings,
+ gsettings-ubuntu-schemas (>= 0.0.1+14.04.20140224),
+Recommends: unity-control-center | gnome-control-center | ubuntu-system-settings | pavucontrol,
Description: System sound indicator.
System sound indicator which provides easy control of the PulseAudio sound
daemon.
diff --git a/debian/rules b/debian/rules
index 6df5436..8fb0f91 100755
--- a/debian/rules
+++ b/debian/rules
@@ -5,6 +5,11 @@ export DPKG_GENSYMBOLS_CHECK_LEVEL=4
%:
dh $@ --parallel --fail-missing --with translations
+override_dh_auto_configure:
+ # Debian defines CMAKE_INSTALL_LOCALSTATEDIR as /usr/var, which is wrong.
+ # So until Debian bug 719148 is fixed, do it ourselves.
+ dh_auto_configure -- -DCMAKE_INSTALL_LOCALSTATEDIR="/var"
+
override_dh_install:
mkdir -p debian/indicator-sound/usr/share/apport/package-hooks/
install -m 644 debian/source_indicator-sound.py debian/indicator-sound/usr/share/apport/package-hooks/
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c11ec51..280e89a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -5,8 +5,20 @@
set(HEADER_PATH "${CMAKE_CURRENT_BINARY_DIR}/indicator-sound-service.h")
set(SYMBOLS_PATH "${CMAKE_CURRENT_BINARY_DIR}/indicator-sound-service.def")
+set(VAPI_PATH "${CMAKE_CURRENT_BINARY_DIR}/indicator-sound-service.vapi")
+
+vapi_gen(accounts-service
+ LIBRARY
+ accounts-service
+ PACKAGES
+ gio-2.0
+ INPUT
+ /usr/share/gir-1.0/AccountsService-1.0.gir
+)
vala_init(indicator-sound-service
+ DEPENDS
+ accounts-service
PACKAGES
config
gio-2.0
@@ -15,12 +27,12 @@ vala_init(indicator-sound-service
libpulse
libpulse-mainloop-glib
libnotify
+ accounts-service
OPTIONS
--ccode
--thread
--vapidir=${CMAKE_SOURCE_DIR}/vapi/
--vapidir=.
- --target-glib=2.36
--pkg=url-dispatcher
--pkg=bus-watcher
)
@@ -33,24 +45,25 @@ vala_add(indicator-sound-service
media-player
media-player-list
mpris2-interfaces
-)
-vala_add(indicator-sound-service
- main.vala
- DEPENDS
- service
+ accounts-service-user
)
vala_add(indicator-sound-service
volume-control.vala
)
vala_add(indicator-sound-service
media-player.vala
+)
+vala_add(indicator-sound-service
+ media-player-mpris.vala
DEPENDS
+ media-player
mpris2-interfaces
)
vala_add(indicator-sound-service
media-player-list.vala
DEPENDS
media-player
+ media-player-mpris
mpris2-interfaces
)
vala_add(indicator-sound-service
@@ -65,6 +78,16 @@ vala_add(indicator-sound-service
media-player
mpris2-interfaces
)
+vala_add(indicator-sound-service
+ accounts-service-user.vala
+ DEPENDS
+ media-player
+ mpris2-interfaces
+ accounts-service-sound-settings
+)
+vala_add(indicator-sound-service
+ accounts-service-sound-settings.vala
+)
vala_finish(indicator-sound-service
SOURCES
@@ -75,6 +98,8 @@ vala_finish(indicator-sound-service
${HEADER_PATH}
GENERATE_SYMBOLS
${SYMBOLS_PATH}
+ GENERATE_VAPI
+ ${VAPI_PATH}
)
set_source_files_properties(
@@ -92,16 +117,33 @@ set(
)
###########################
-# Executable
+# Lib
###########################
add_definitions(
-w
)
+add_library(
+ indicator-sound-service-lib STATIC
+ ${INDICATOR_SOUND_SOURCES}
+)
+
+target_link_libraries(
+ indicator-sound-service-lib
+ ${PULSEAUDIO_LIBRARIES}
+ ${SOUNDSERVICE_LIBRARIES}
+)
+
+###########################
+# Executable
+###########################
+
+include_directories(${CMAKE_BINARY_DIR})
+
add_executable(
indicator-sound-service-bin
- ${INDICATOR_SOUND_SOURCES}
+ main.c
)
set_target_properties(
@@ -112,8 +154,7 @@ set_target_properties(
target_link_libraries(
indicator-sound-service-bin
- ${PULSEAUDIO_LIBRARIES}
- ${SOUNDSERVICE_LIBRARIES}
+ indicator-sound-service-lib
)
###########################
diff --git a/src/accounts-service-sound-settings.vala b/src/accounts-service-sound-settings.vala
new file mode 100644
index 0000000..7e27bd5
--- /dev/null
+++ b/src/accounts-service-sound-settings.vala
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2014 © Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+[DBus (name = "com.canonical.indicator.sound.AccountsService")]
+public interface AccountsServiceSoundSettings : Object {
+ // properties
+ public abstract uint64 timestamp {owned get; set;}
+ public abstract string player_name {owned get; set;}
+ public abstract Variant player_icon {owned get; set;}
+ public abstract bool running {owned get; set;}
+ public abstract string state {owned get; set;}
+ public abstract string title {owned get; set;}
+ public abstract string artist {owned get; set;}
+ public abstract string album {owned get; set;}
+ public abstract string art_url {owned get; set;}
+}
+
diff --git a/src/accounts-service-user.vala b/src/accounts-service-user.vala
new file mode 100644
index 0000000..c29842a
--- /dev/null
+++ b/src/accounts-service-user.vala
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2014 © Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+public class AccountsServiceUser : Object {
+ Act.UserManager accounts_manager = Act.UserManager.get_default();
+ Act.User? user = null;
+ AccountsServiceSoundSettings? proxy = null;
+ uint timer = 0;
+ MediaPlayer? _player = null;
+
+ public MediaPlayer? player {
+ set {
+ this._player = value;
+ debug("New player: %s", this._player != null ? this._player.name : "Cleared");
+
+ /* No proxy, no settings to set */
+ if (this.proxy == null) {
+ debug("Nothing written to Accounts Service, waiting on proxy");
+ return;
+ }
+
+ /* Always reset the timer */
+ if (this.timer != 0) {
+ GLib.Source.remove(this.timer);
+ this.timer = 0;
+ }
+
+ if (this._player == null) {
+ debug("Clearing player data in accounts service");
+
+ /* Clear it */
+ this.proxy.player_name = "";
+ this.proxy.timestamp = 0;
+ this.proxy.title = "";
+ this.proxy.artist = "";
+ this.proxy.album = "";
+ this.proxy.art_url = "";
+
+ var icon = new ThemedIcon.with_default_fallbacks ("application-default-icon");
+ this.proxy.player_icon = icon.serialize();
+ } else {
+ this.proxy.timestamp = GLib.get_monotonic_time();
+ this.proxy.player_name = this._player.name;
+ if (this._player.icon == null) {
+ var icon = new ThemedIcon.with_default_fallbacks ("application-default-icon");
+ this.proxy.player_icon = icon.serialize();
+ } else {
+ this.proxy.player_icon = this._player.icon.serialize();
+ }
+
+ this.proxy.running = this._player.is_running;
+ this.proxy.state = this._player.state;
+
+ if (this._player.current_track != null) {
+ this.proxy.title = this._player.current_track.title;
+ this.proxy.artist = this._player.current_track.artist;
+ this.proxy.album = this._player.current_track.album;
+ this.proxy.art_url = this._player.current_track.art_url;
+ } else {
+ this.proxy.title = "";
+ this.proxy.artist = "";
+ this.proxy.album = "";
+ this.proxy.art_url = "";
+ }
+
+ this.timer = GLib.Timeout.add_seconds(5 * 60, () => {
+ debug("Writing timestamp");
+ this.proxy.timestamp = GLib.get_monotonic_time();
+ return true;
+ });
+ }
+ }
+ get {
+ return this._player;
+ }
+ }
+
+ public AccountsServiceUser () {
+ user = accounts_manager.get_user(GLib.Environment.get_user_name());
+ user.notify["is-loaded"].connect(() => user_loaded_changed());
+ user_loaded_changed();
+ }
+
+ void user_loaded_changed () {
+ debug("User loaded changed");
+
+ this.proxy = null;
+
+ if (this.user.is_loaded) {
+ Bus.get_proxy.begin<AccountsServiceSoundSettings> (
+ BusType.SYSTEM,
+ "org.freedesktop.Accounts",
+ user.get_object_path(),
+ DBusProxyFlags.GET_INVALIDATED_PROPERTIES,
+ null,
+ new_proxy);
+ }
+ }
+
+ ~AccountsServiceUser () {
+ debug("Account Service Object Finalizing");
+ this.player = null;
+
+ if (this.timer != 0) {
+ GLib.Source.remove(this.timer);
+ this.timer = 0;
+ }
+ }
+
+ void new_proxy (GLib.Object? obj, AsyncResult res) {
+ try {
+ this.proxy = Bus.get_proxy.end (res);
+ this.player = _player;
+ } catch (Error e) {
+ this.proxy = null;
+ warning("Unable to get proxy to user sound settings: %s", e.message);
+ }
+ }
+}
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..f8635c8
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,32 @@
+/* main.c generated by valac 0.22.1, the Vala compiler
+ * generated from main.vala, do not modify */
+
+
+#include <glib.h>
+#include <locale.h>
+#include <libnotify/notify.h>
+
+#include "indicator-sound-service.h"
+#include "config.h"
+
+int
+main (int argc, char ** argv) {
+ gint result = 0;
+ IndicatorSoundService* service = NULL;
+
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ setlocale (LC_ALL, "");
+ bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
+
+ /* Initialize libnotify */
+ notify_init ("indicator-sound");
+
+
+ service = indicator_sound_service_new ();
+ result = indicator_sound_service_run (service);
+ g_object_unref(service);
+
+ return result;
+}
+
+
diff --git a/src/main.vala b/src/main.vala
deleted file mode 100644
index 4da9e58..0000000
--- a/src/main.vala
+++ /dev/null
@@ -1,14 +0,0 @@
-
-[CCode (cheader_filename="libintl.h", type="char *")]
-extern unowned string bind_textdomain_codeset (string domainname, string codeset);
-
-static int main (string[] args) {
- bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8");
- Intl.setlocale (LocaleCategory.ALL, "");
- Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.GNOMELOCALEDIR);
-
- Notify.init ("indicator-sound");
-
- var service = new IndicatorSound.Service ();
- return service.run ();
-}
diff --git a/src/media-player-list.vala b/src/media-player-list.vala
index 5a12e32..87ca1f0 100644
--- a/src/media-player-list.vala
+++ b/src/media-player-list.vala
@@ -24,24 +24,24 @@
public class MediaPlayerList {
public MediaPlayerList () {
- this._players = new HashTable<string, MediaPlayer> (str_hash, str_equal);
+ this._players = new HashTable<string, MediaPlayerMpris> (str_hash, str_equal);
BusWatcher.watch_namespace (BusType.SESSION, "org.mpris.MediaPlayer2", this.player_appeared, this.player_disappeared);
}
/* only valid while the list is not changed */
public class Iterator {
- HashTableIter<string, MediaPlayer> iter;
+ HashTableIter<string, MediaPlayerMpris> iter;
public Iterator (MediaPlayerList list) {
- this.iter = HashTableIter<string, MediaPlayer> (list._players);
+ this.iter = HashTableIter<string, MediaPlayerMpris> (list._players);
}
public MediaPlayer? next_value () {
- MediaPlayer? player;
+ MediaPlayerMpris? player;
if (this.iter.next (null, out player))
- return player;
+ return player as MediaPlayer;
else
return null;
}
@@ -54,9 +54,9 @@ public class MediaPlayerList {
/**
* Adds the player associated with @desktop_id. Does nothing if such a player already exists.
*/
- MediaPlayer? insert (string desktop_id) {
+ MediaPlayerMpris? insert (string desktop_id) {
var id = desktop_id.has_suffix (".desktop") ? desktop_id : desktop_id + ".desktop";
- MediaPlayer? player = this._players.lookup (id);
+ MediaPlayerMpris? player = this._players.lookup (id);
if (player == null) {
var appinfo = new DesktopAppInfo (id);
@@ -65,7 +65,7 @@ public class MediaPlayerList {
return null;
}
- player = new MediaPlayer (appinfo);
+ player = new MediaPlayerMpris (appinfo);
this._players.insert (player.id, player);
this.player_added (player);
}
@@ -110,7 +110,7 @@ public class MediaPlayerList {
public signal void player_added (MediaPlayer player);
public signal void player_removed (MediaPlayer player);
- HashTable<string, MediaPlayer> _players;
+ HashTable<string, MediaPlayerMpris> _players;
void player_appeared (DBusConnection connection, string name, string owner) {
try {
@@ -126,7 +126,7 @@ public class MediaPlayerList {
}
void player_disappeared (DBusConnection connection, string dbus_name) {
- MediaPlayer? player = this._players.find ( (name, player) => {
+ MediaPlayerMpris? player = this._players.find ( (name, player) => {
return player.dbus_name == dbus_name;
});
diff --git a/src/media-player-mpris.vala b/src/media-player-mpris.vala
new file mode 100644
index 0000000..25ddac4
--- /dev/null
+++ b/src/media-player-mpris.vala
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Lars Uebernickel <lars.uebernickel@canonical.com>
+ */
+
+/**
+ * MediaPlayerMpris represents an MRPIS-capable media player.
+ */
+public class MediaPlayerMpris: MediaPlayer {
+
+ public MediaPlayerMpris (DesktopAppInfo appinfo) {
+ this.appinfo = appinfo;
+ }
+
+ /** Desktop id of the player */
+ public override string id {
+ get {
+ return this.appinfo.get_id ();
+ }
+ }
+
+ /** Display name of the player */
+ public override string name {
+ get {
+ return this.appinfo.get_name ();
+ }
+ }
+
+ /** Application icon of the player */
+ public override Icon? icon {
+ get {
+ return this.appinfo.get_icon ();
+ }
+ }
+
+ /**
+ * True if an instance of the player is currently running.
+ *
+ * See also: attach(), detach()
+ */
+ public override bool is_running {
+ get {
+ return this.proxy != null;
+ }
+ }
+
+ /** Name of the player on the bus, if an instance is currently running */
+ public override string dbus_name {
+ get {
+ return this._dbus_name;
+ }
+ }
+
+ public override string state {
+ get; private set; default = "Paused";
+ }
+
+ public override MediaPlayer.Track? current_track {
+ get; set;
+ }
+
+ public override bool can_raise {
+ get {
+ return this.root != null ? this.root.CanRaise : true;
+ }
+ }
+
+ /**
+ * Attach this object to a process of the associated media player. The player must own @dbus_name and
+ * implement the org.mpris.MediaPlayer2.Player interface.
+ *
+ * Only one player can be attached at any given time. Use detach() to detach a player.
+ *
+ * This method does not block. If it is successful, "is-running" will be set to %TRUE.
+ */
+ public void attach (MprisRoot root, string dbus_name) {
+ return_if_fail (this._dbus_name == null && this.proxy == null);
+
+ this.root = root;
+ this.notify_property ("can-raise");
+
+ this._dbus_name = dbus_name;
+ Bus.get_proxy.begin<MprisPlayer> (BusType.SESSION, dbus_name, "/org/mpris/MediaPlayer2",
+ DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, got_proxy);
+ Bus.get_proxy.begin<MprisPlaylists> (BusType.SESSION, dbus_name, "/org/mpris/MediaPlayer2",
+ DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, got_playlists_proxy);
+ }
+
+ /**
+ * Detach this object from a process running the associated media player.
+ *
+ * See also: attach()
+ */
+ public void detach () {
+ this.root = null;
+ this.proxy = null;
+ this._dbus_name = null;
+ this.notify_property ("is-running");
+ this.notify_property ("can-raise");
+ this.state = "Paused";
+ this.current_track = null;
+ }
+
+ /**
+ * Activate the associated media player.
+ *
+ * Note: this will _not_ call attach(), because it doesn't know on which dbus-name the player will appear.
+ * Use attach() to attach this object to a running instance of the player.
+ */
+ public override void activate () {
+ try {
+ if (this.proxy == null) {
+ this.appinfo.launch (null, null);
+ this.state = "Launching";
+ }
+ else if (this.root != null && this.root.CanRaise) {
+ this.root.Raise ();
+ }
+ }
+ catch (Error e) {
+ warning ("unable to activate %s: %s", appinfo.get_name (), e.message);
+ }
+ }
+
+ /**
+ * Toggles playing status.
+ */
+ public override void play_pause () {
+ if (this.proxy != null) {
+ this.proxy.PlayPause.begin ();
+ }
+ else if (this.state != "Launching") {
+ this.play_when_attached = true;
+ this.activate ();
+ }
+ }
+
+ /**
+ * Skips to the next track.
+ */
+ public override void next () {
+ if (this.proxy != null)
+ this.proxy.Next.begin ();
+ }
+
+ /**
+ * Skips to the previous track.
+ */
+ public override void previous () {
+ if (this.proxy != null)
+ this.proxy.Previous.begin ();
+ }
+
+ public override uint get_n_playlists () {
+ return this.playlists != null ? this.playlists.length : 0;
+ }
+
+ public override string get_playlist_id (int index) {
+ return_val_if_fail (index < this.playlists.length, "");
+ return this.playlists[index].path;
+ }
+
+ public override string get_playlist_name (int index) {
+ return_val_if_fail (index < this.playlists.length, "");
+ return this.playlists[index].name;
+ }
+
+ public override void activate_playlist_by_name (string name) {
+ if (this.playlists_proxy != null)
+ this.playlists_proxy.ActivatePlaylist.begin (new ObjectPath (name));
+ }
+
+ DesktopAppInfo appinfo;
+ MprisPlayer? proxy;
+ MprisPlaylists ?playlists_proxy;
+ string _dbus_name;
+ bool play_when_attached = false;
+ MprisRoot root;
+ PlaylistDetails[] playlists = null;
+
+ void got_proxy (Object? obj, AsyncResult res) {
+ try {
+ this.proxy = Bus.get_proxy.end (res);
+
+ /* Connecting to GDBusProxy's "g-properties-changed" signal here, because vala's dbus objects don't
+ * emit notify signals */
+ var gproxy = this.proxy as DBusProxy;
+ gproxy.g_properties_changed.connect (this.proxy_properties_changed);
+
+ this.notify_property ("is-running");
+ this.state = this.proxy.PlaybackStatus;
+ this.update_current_track (gproxy.get_cached_property ("Metadata"));
+
+ if (this.play_when_attached) {
+ /* wait a little before calling PlayPause, some players need some time to
+ set themselves up */
+ Timeout.add (1000, () => { proxy.PlayPause.begin (); return false; } );
+ this.play_when_attached = false;
+ }
+ }
+ catch (Error e) {
+ this._dbus_name = null;
+ warning ("unable to attach to media player: %s", e.message);
+ }
+ }
+
+ void fetch_playlists () {
+ /* The proxy is created even when the interface is not supported. GDBusProxy will
+ return 0 for the PlaylistCount property in that case. */
+ if (this.playlists_proxy != null && this.playlists_proxy.PlaylistCount > 0) {
+ this.playlists_proxy.GetPlaylists.begin (0, 100, "Alphabetical", false, (obj, res) => {
+ try {
+ this.playlists = playlists_proxy.GetPlaylists.end (res);
+ this.playlists_changed ();
+ }
+ catch (Error e) {
+ warning ("could not fetch playlists: %s", e.message);
+ this.playlists = null;
+ }
+ });
+ }
+ else {
+ this.playlists = null;
+ this.playlists_changed ();
+ }
+ }
+
+ void got_playlists_proxy (Object? obj, AsyncResult res) {
+ try {
+ this.playlists_proxy = Bus.get_proxy.end (res);
+
+ var gproxy = this.proxy as DBusProxy;
+ gproxy.g_properties_changed.connect (this.playlists_proxy_properties_changed);
+ }
+ catch (Error e) {
+ warning ("unable to create mpris plalists proxy: %s", e.message);
+ return;
+ }
+
+ Timeout.add (500, () => { this.fetch_playlists (); return false; } );
+ }
+
+ /* some players (e.g. Spotify) don't follow the spec closely and pass single strings in metadata fields
+ * where an array of string is expected */
+ static string sanitize_metadata_value (Variant? v) {
+ if (v == null)
+ return "";
+ else if (v.is_of_type (VariantType.STRING))
+ return v.get_string ();
+ else if (v.is_of_type (VariantType.STRING_ARRAY))
+ return string.joinv (",", v.get_strv ());
+
+ warn_if_reached ();
+ return "";
+ }
+
+ void proxy_properties_changed (DBusProxy proxy, Variant changed_properties, string[] invalidated_properties) {
+ if (changed_properties.lookup ("PlaybackStatus", "s", null)) {
+ this.state = this.proxy.PlaybackStatus;
+ }
+
+ var metadata = changed_properties.lookup_value ("Metadata", new VariantType ("a{sv}"));
+ if (metadata != null)
+ this.update_current_track (metadata);
+ }
+
+ void playlists_proxy_properties_changed (DBusProxy proxy, Variant changed_properties, string[] invalidated_properties) {
+ if (changed_properties.lookup ("PlaylistCount", "u", null))
+ this.fetch_playlists ();
+ }
+
+ void update_current_track (Variant metadata) {
+ if (metadata != null) {
+ this.current_track = new Track (
+ sanitize_metadata_value (metadata.lookup_value ("xesam:artist", null)),
+ sanitize_metadata_value (metadata.lookup_value ("xesam:title", null)),
+ sanitize_metadata_value (metadata.lookup_value ("xesam:album", null)),
+ sanitize_metadata_value (metadata.lookup_value ("mpris:artUrl", null))
+ );
+ }
+ else {
+ this.current_track = null;
+ }
+ }
+}
diff --git a/src/media-player.vala b/src/media-player.vala
index da68ac1..4d4aef3 100644
--- a/src/media-player.vala
+++ b/src/media-player.vala
@@ -1,5 +1,5 @@
/*
- * Copyright 2013 Canonical Ltd.
+ * Copyright © 2014 Canonical Ltd.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,60 +14,18 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
- * Lars Uebernickel <lars.uebernickel@canonical.com>
+ * Ted Gould <ted@canonical.com>
*/
-/**
- * MediaPlayer represents an MRPIS-capable media player.
- */
-public class MediaPlayer: Object {
-
- public MediaPlayer (DesktopAppInfo appinfo) {
- this.appinfo = appinfo;
- }
-
- /** Desktop id of the player */
- public string id {
- get {
- return this.appinfo.get_id ();
- }
- }
+public abstract class MediaPlayer : Object {
+ public virtual string id { get { not_implemented(); return ""; } }
+ public virtual string name { get { not_implemented(); return ""; } }
+ public virtual string state { get { not_implemented(); return ""; } set { }}
+ public virtual Icon? icon { get { not_implemented(); return null; } }
+ public virtual string dbus_name { get { not_implemented(); return ""; } }
- /** Display name of the player */
- public string name {
- get {
- return this.appinfo.get_name ();
- }
- }
-
- /** Application icon of the player */
- public Icon icon {
- get {
- return this.appinfo.get_icon ();
- }
- }
-
- /**
- * True if an instance of the player is currently running.
- *
- * See also: attach(), detach()
- */
- public bool is_running {
- get {
- return this.proxy != null;
- }
- }
-
- /** Name of the player on the bus, if an instance is currently running */
- public string dbus_name {
- get {
- return this._dbus_name;
- }
- }
-
- public string state {
- get; private set; default = "Paused";
- }
+ public virtual bool is_running { get { not_implemented(); return false; } }
+ public virtual bool can_raise { get { not_implemented(); return false; } }
public class Track : Object {
public string artist { get; construct; }
@@ -80,233 +38,24 @@ public class MediaPlayer: Object {
}
}
- public Track current_track {
- get; set;
- }
-
- public bool can_raise {
- get {
- return this.root != null ? this.root.CanRaise : true;
- }
+ public virtual Track? current_track {
+ get { not_implemented(); return null; }
+ set { not_implemented(); }
}
public signal void playlists_changed ();
- /**
- * Attach this object to a process of the associated media player. The player must own @dbus_name and
- * implement the org.mpris.MediaPlayer2.Player interface.
- *
- * Only one player can be attached at any given time. Use detach() to detach a player.
- *
- * This method does not block. If it is successful, "is-running" will be set to %TRUE.
- */
- public void attach (MprisRoot root, string dbus_name) {
- return_if_fail (this._dbus_name == null && this.proxy == null);
-
- this.root = root;
- this.notify_property ("can-raise");
-
- this._dbus_name = dbus_name;
- Bus.get_proxy.begin<MprisPlayer> (BusType.SESSION, dbus_name, "/org/mpris/MediaPlayer2",
- DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, got_proxy);
- Bus.get_proxy.begin<MprisPlaylists> (BusType.SESSION, dbus_name, "/org/mpris/MediaPlayer2",
- DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, got_playlists_proxy);
- }
-
- /**
- * Detach this object from a process running the associated media player.
- *
- * See also: attach()
- */
- public void detach () {
- this.root = null;
- this.proxy = null;
- this._dbus_name = null;
- this.notify_property ("is-running");
- this.notify_property ("can-raise");
- this.state = "Paused";
- this.current_track = null;
- }
+ public abstract void activate ();
+ public abstract void play_pause ();
+ public abstract void next ();
+ public abstract void previous ();
- /**
- * Activate the associated media player.
- *
- * Note: this will _not_ call attach(), because it doesn't know on which dbus-name the player will appear.
- * Use attach() to attach this object to a running instance of the player.
- */
- public void activate () {
- try {
- if (this.proxy == null) {
- this.appinfo.launch (null, null);
- this.state = "Launching";
- }
- else if (this.root != null && this.root.CanRaise) {
- this.root.Raise ();
- }
- }
- catch (Error e) {
- warning ("unable to activate %s: %s", appinfo.get_name (), e.message);
- }
- }
-
- /**
- * Toggles playing status.
- */
- public void play_pause () {
- if (this.proxy != null) {
- this.proxy.PlayPause.begin ();
- }
- else if (this.state != "Launching") {
- this.play_when_attached = true;
- this.activate ();
- }
- }
-
- /**
- * Skips to the next track.
- */
- public void next () {
- if (this.proxy != null)
- this.proxy.Next.begin ();
- }
-
- /**
- * Skips to the previous track.
- */
- public void previous () {
- if (this.proxy != null)
- this.proxy.Previous.begin ();
- }
+ public abstract uint get_n_playlists();
+ public abstract string get_playlist_id (int index);
+ public abstract string get_playlist_name (int index);
+ public abstract void activate_playlist_by_name (string playlist);
- public uint get_n_playlists () {
- return this.playlists != null ? this.playlists.length : 0;
- }
-
- public string get_playlist_id (int index) {
- return_val_if_fail (index < this.playlists.length, "");
- return this.playlists[index].path;
- }
-
- public string get_playlist_name (int index) {
- return_val_if_fail (index < this.playlists.length, "");
- return this.playlists[index].name;
- }
-
- public void activate_playlist_by_name (string name) {
- if (this.playlists_proxy != null)
- this.playlists_proxy.ActivatePlaylist.begin (new ObjectPath (name));
- }
-
- DesktopAppInfo appinfo;
- MprisPlayer? proxy;
- MprisPlaylists ?playlists_proxy;
- string _dbus_name;
- bool play_when_attached = false;
- MprisRoot root;
- PlaylistDetails[] playlists = null;
-
- void got_proxy (Object? obj, AsyncResult res) {
- try {
- this.proxy = Bus.get_proxy.end (res);
-
- /* Connecting to GDBusProxy's "g-properties-changed" signal here, because vala's dbus objects don't
- * emit notify signals */
- var gproxy = this.proxy as DBusProxy;
- gproxy.g_properties_changed.connect (this.proxy_properties_changed);
-
- this.notify_property ("is-running");
- this.state = this.proxy.PlaybackStatus;
- this.update_current_track (gproxy.get_cached_property ("Metadata"));
-
- if (this.play_when_attached) {
- /* wait a little before calling PlayPause, some players need some time to
- set themselves up */
- Timeout.add (1000, () => { proxy.PlayPause.begin (); return false; } );
- this.play_when_attached = false;
- }
- }
- catch (Error e) {
- this._dbus_name = null;
- warning ("unable to attach to media player: %s", e.message);
- }
- }
-
- void fetch_playlists () {
- /* The proxy is created even when the interface is not supported. GDBusProxy will
- return 0 for the PlaylistCount property in that case. */
- if (this.playlists_proxy != null && this.playlists_proxy.PlaylistCount > 0) {
- this.playlists_proxy.GetPlaylists.begin (0, 100, "Alphabetical", false, (obj, res) => {
- try {
- this.playlists = playlists_proxy.GetPlaylists.end (res);
- this.playlists_changed ();
- }
- catch (Error e) {
- warning ("could not fetch playlists: %s", e.message);
- this.playlists = null;
- }
- });
- }
- else {
- this.playlists = null;
- this.playlists_changed ();
- }
- }
-
- void got_playlists_proxy (Object? obj, AsyncResult res) {
- try {
- this.playlists_proxy = Bus.get_proxy.end (res);
-
- var gproxy = this.proxy as DBusProxy;
- gproxy.g_properties_changed.connect (this.playlists_proxy_properties_changed);
- }
- catch (Error e) {
- warning ("unable to create mpris plalists proxy: %s", e.message);
- return;
- }
-
- Timeout.add (500, () => { this.fetch_playlists (); return false; } );
- }
-
- /* some players (e.g. Spotify) don't follow the spec closely and pass single strings in metadata fields
- * where an array of string is expected */
- static string sanitize_metadata_value (Variant? v) {
- if (v == null)
- return "";
- else if (v.is_of_type (VariantType.STRING))
- return v.get_string ();
- else if (v.is_of_type (VariantType.STRING_ARRAY))
- return string.joinv (",", v.get_strv ());
-
- warn_if_reached ();
- return "";
- }
-
- void proxy_properties_changed (DBusProxy proxy, Variant changed_properties, string[] invalidated_properties) {
- if (changed_properties.lookup ("PlaybackStatus", "s", null)) {
- this.state = this.proxy.PlaybackStatus;
- }
-
- var metadata = changed_properties.lookup_value ("Metadata", new VariantType ("a{sv}"));
- if (metadata != null)
- this.update_current_track (metadata);
- }
-
- void playlists_proxy_properties_changed (DBusProxy proxy, Variant changed_properties, string[] invalidated_properties) {
- if (changed_properties.lookup ("PlaylistCount", "u", null))
- this.fetch_playlists ();
- }
-
- void update_current_track (Variant metadata) {
- if (metadata != null) {
- this.current_track = new Track (
- sanitize_metadata_value (metadata.lookup_value ("xesam:artist", null)),
- sanitize_metadata_value (metadata.lookup_value ("xesam:title", null)),
- sanitize_metadata_value (metadata.lookup_value ("xesam:album", null)),
- sanitize_metadata_value (metadata.lookup_value ("mpris:artUrl", null))
- );
- }
- else {
- this.current_track = null;
- }
+ private void not_implemented () {
+ warning("Property not implemented");
}
}
diff --git a/src/service.vala b/src/service.vala
index 25a7b18..f6c5f01 100644
--- a/src/service.vala
+++ b/src/service.vala
@@ -17,9 +17,13 @@
* Lars Uebernickel <lars.uebernickel@canonical.com>
*/
-public class IndicatorSound.Service {
+public class IndicatorSound.Service: Object {
public Service () {
this.settings = new Settings ("com.canonical.indicator.sound");
+ this.sharedsettings = new Settings ("com.ubuntu.sound");
+
+ this.settings.bind ("visible", this, "visible", SettingsBindFlags.GET);
+ this.notify["visible"].connect ( () => this.update_root_icon () );
this.volume_control = new VolumeControl ();
@@ -34,7 +38,7 @@ public class IndicatorSound.Service {
this.actions.add_action (this.create_mic_volume_action ());
this.menus = new HashTable<string, SoundMenu> (str_hash, str_equal);
- this.menus.insert ("desktop_greeter", new SoundMenu (null, SoundMenu.DisplayFlags.SHOW_MUTE));
+ this.menus.insert ("desktop_greeter", new SoundMenu (null, SoundMenu.DisplayFlags.SHOW_MUTE | SoundMenu.DisplayFlags.HIDE_PLAYERS));
this.menus.insert ("desktop", new SoundMenu ("indicator.desktop-settings", SoundMenu.DisplayFlags.SHOW_MUTE));
this.menus.insert ("phone", new SoundMenu ("indicator.phone-settings", SoundMenu.DisplayFlags.HIDE_INACTIVE_PLAYERS));
@@ -42,9 +46,13 @@ public class IndicatorSound.Service {
this.volume_control.bind_property ("active-mic", menu, "show-mic-volume", BindingFlags.SYNC_CREATE);
});
- this.players.sync (settings.get_strv ("interested-media-players"));
+ /* Setup handling for the greeter-export setting */
+ this.settings.changed["greeter-export"].connect( () => this.build_accountsservice() );
+ build_accountsservice();
+
+ this.sync_preferred_players ();
this.settings.changed["interested-media-players"].connect ( () => {
- this.players.sync (settings.get_strv ("interested-media-players"));
+ this.sync_preferred_players ();
});
if (settings.get_boolean ("show-notify-osd-on-scroll")) {
@@ -54,6 +62,43 @@ public class IndicatorSound.Service {
this.notification.set_hint_string ("x-canonical-private-synchronous", "indicator-sound");
}
}
+
+ sharedsettings.bind ("allow-amplified-volume", this, "allow-amplified-volume", SettingsBindFlags.GET);
+ }
+
+ ~Service() {
+ if (this.sound_was_blocked_timeout_id > 0) {
+ Source.remove (this.sound_was_blocked_timeout_id);
+ this.sound_was_blocked_timeout_id = 0;
+ }
+ }
+
+ void build_accountsservice () {
+ clear_acts_player();
+ this.accounts_service = null;
+
+ /* If we're not exporting, don't build anything */
+ if (!this.settings.get_boolean("greeter-export")) {
+ debug("Accounts service export disabled due to user setting");
+ return;
+ }
+
+ /* If we're on the greeter, don't export */
+ if (GLib.Environment.get_user_name() == "lightdm") {
+ debug("Accounts service export disabled due to being used on the greeter");
+ return;
+ }
+
+ this.accounts_service = new AccountsServiceUser();
+
+ this.eventually_update_player_actions();
+ }
+
+ void clear_acts_player () {
+ /* NOTE: This is a bit of a hack to ensure that accounts service doesn't
+ continue to export the player by keeping a ref in the timer */
+ if (this.accounts_service != null)
+ this.accounts_service.player = null;
}
public int run () {
@@ -66,11 +111,41 @@ public class IndicatorSound.Service {
this.bus_acquired, null, this.name_lost);
this.loop = new MainLoop (null, false);
+
+ GLib.Unix.signal_add(GLib.ProcessSignal.TERM, () => {
+ debug("SIGTERM recieved, stopping our mainloop");
+ this.loop.quit();
+ return false;
+ });
+
this.loop.run ();
+ clear_acts_player();
+
return 0;
}
+ public bool visible { get; set; }
+
+ public bool allow_amplified_volume {
+ get {
+ return this.max_volume > 1.0;
+ }
+
+ set {
+ if (value) {
+ /* from pulse/volume.h: #define PA_VOLUME_UI_MAX (pa_sw_volume_from_dB(+11.0)) */
+ this.max_volume = (double)PulseAudio.Volume.sw_from_dB(11.0) / PulseAudio.Volume.NORM;
+ }
+ else {
+ this.max_volume = 1.0;
+ }
+
+ /* Normalize volume, because the volume action's state is [0.0, 1.0], see create_volume_action() */
+ this.actions.change_action_state ("volume", this.volume_control.get_volume () / this.max_volume);
+ }
+ }
+
const ActionEntry[] action_entries = {
{ "root", null, null, "@a{sv} {}", null },
{ "scroll", activate_scroll_action, "i", null, null },
@@ -82,10 +157,20 @@ public class IndicatorSound.Service {
SimpleActionGroup actions;
HashTable<string, SoundMenu> menus;
Settings settings;
+ Settings sharedsettings;
VolumeControl volume_control;
MediaPlayerList players;
uint player_action_update_id;
+ bool mute_blocks_sound;
+ uint sound_was_blocked_timeout_id;
Notify.Notification notification;
+ bool syncing_preferred_players = false;
+ AccountsServiceUser? accounts_service = null;
+
+ /* Maximum volume as a scaling factor between the volume action's state and the value in
+ * this.volume_control. See create_volume_action().
+ */
+ double max_volume = 1.0;
const double volume_step_percentage = 0.06;
@@ -93,7 +178,7 @@ public class IndicatorSound.Service {
int delta = param.get_int32(); /* positive for up, negative for down */
double v = this.volume_control.get_volume () + volume_step_percentage * delta;
- this.volume_control.set_volume (v.clamp (0.0, 1.0));
+ this.volume_control.set_volume (v.clamp (0.0, this.max_volume));
if (this.notification != null) {
string icon;
@@ -107,7 +192,7 @@ public class IndicatorSound.Service {
icon = "notification-audio-volume-high";
this.notification.update ("indicator-sound", "", icon);
- this.notification.set_hint_int32 ("value", ((int32) (100 * v)).clamp (-1, 101));
+ this.notification.set_hint_int32 ("value", ((int32) (100 * v / this.max_volume)).clamp (-1, 101));
try {
this.notification.show ();
}
@@ -123,7 +208,12 @@ public class IndicatorSound.Service {
if (env == "xubuntu" || env == "ubuntustudio")
cmd = "pavucontrol";
else
- cmd = "gnome-control-center sound";
+ {
+ if (Environment.get_variable ("XDG_CURRENT_DESKTOP") == "Unity" && Environment.find_program_in_path ("unity-control-center") != null)
+ cmd = "unity-control-center sound";
+ else
+ cmd = "gnome-control-center sound";
+ }
try {
Process.spawn_command_line_async (cmd);
@@ -147,7 +237,7 @@ public class IndicatorSound.Service {
double volume = this.volume_control.get_volume ();
string icon;
if (this.volume_control.mute)
- icon = "audio-volume-muted-panel";
+ icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel";
else if (volume <= 0.0)
icon = "audio-volume-low-zero-panel";
else if (volume <= 0.3)
@@ -170,7 +260,7 @@ public class IndicatorSound.Service {
builder.add ("{sv}", "title", new Variant.string (_("Sound")));
builder.add ("{sv}", "accessible-desc", new Variant.string (accessible_name));
builder.add ("{sv}", "icon", serialize_themed_icon (icon));
- builder.add ("{sv}", "visible", new Variant.boolean (true));
+ builder.add ("{sv}", "visible", new Variant.boolean (this.visible));
root_action.set_state (builder.end());
}
@@ -190,27 +280,65 @@ public class IndicatorSound.Service {
this.update_root_icon ();
});
+ this.volume_control.notify["is-playing"].connect( () => {
+ if (!this.volume_control.mute) {
+ this.mute_blocks_sound = false;
+ return;
+ }
+
+ if (this.volume_control.is_playing) {
+ this.mute_blocks_sound = true;
+ }
+ else if (this.mute_blocks_sound) {
+ /* Continue to show the blocking icon five seconds after a player has tried to play something */
+ if (this.sound_was_blocked_timeout_id > 0)
+ Source.remove (this.sound_was_blocked_timeout_id);
+
+ this.sound_was_blocked_timeout_id = Timeout.add_seconds (5, () => {
+ this.mute_blocks_sound = false;
+ this.sound_was_blocked_timeout_id = 0;
+ this.update_root_icon ();
+ return false;
+ });
+ }
+
+ this.update_root_icon ();
+ });
+
return mute_action;
}
void volume_changed (double volume) {
var volume_action = this.actions.lookup_action ("volume") as SimpleAction;
- volume_action.set_state (new Variant.double (volume));
+
+ /* Normalize volume, because the volume action's state is [0.0, 1.0], see create_volume_action() */
+ volume_action.set_state (new Variant.double (volume / this.max_volume));
this.update_root_icon ();
}
Action create_volume_action () {
- var volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, new Variant.double (this.volume_control.get_volume ()));
+ /* The action's state is between be in [0.0, 1.0] instead of [0.0,
+ * max_volume], so that we don't need to update the slider menu item
+ * every time allow-amplified-volume is changed. Convert between the
+ * two here, so that we always pass the full range into
+ * volume_control.set_volume().
+ */
+
+ double volume = this.volume_control.get_volume () / this.max_volume;
+
+ var volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, new Variant.double (volume));
volume_action.change_state.connect ( (action, val) => {
- volume_control.set_volume (val.get_double ());
+ double v = val.get_double () * this.max_volume;
+ volume_control.set_volume (v.clamp (0.0, this.max_volume));
});
/* activating this action changes the volume by the amount given in the parameter */
volume_action.activate.connect ( (action, param) => {
- double v = volume_control.get_volume () + volume_step_percentage * param.get_int32 ();
- volume_control.set_volume (v.clamp (0.0, 1.0));
+ int delta = param.get_int32 ();
+ double v = volume_control.get_volume () + volume_step_percentage * delta;
+ volume_control.set_volume (v.clamp (0.0, this.max_volume));
});
this.volume_control.volume_changed.connect (volume_changed);
@@ -264,14 +392,25 @@ public class IndicatorSound.Service {
}
bool update_player_actions () {
+ bool clear_accounts_player = true;
+
foreach (var player in this.players) {
SimpleAction? action = this.actions.lookup_action (player.id) as SimpleAction;
if (action != null) {
action.set_state (this.action_state_for_player (player));
action.set_enabled (player.can_raise);
}
+
+ /* If we're playing then put that data in accounts service */
+ if (player.is_running && accounts_service != null) {
+ accounts_service.player = player;
+ clear_accounts_player = false;
+ }
}
+ if (clear_accounts_player)
+ clear_acts_player();
+
this.player_action_update_id = 0;
return false;
}
@@ -281,11 +420,21 @@ public class IndicatorSound.Service {
this.player_action_update_id = Idle.add (this.update_player_actions);
}
+
+ void sync_preferred_players () {
+ this.syncing_preferred_players = true;
+ this.players.sync (settings.get_strv ("interested-media-players"));
+ this.syncing_preferred_players = false;
+ }
+
void update_preferred_players () {
- var builder = new VariantBuilder (VariantType.STRING_ARRAY);
- foreach (var player in this.players)
- builder.add ("s", player.id);
- this.settings.set_value ("interested-media-players", builder.end ());
+ /* only write the key if we're not getting this call because we're syncing from the key right now */
+ if (!this.syncing_preferred_players) {
+ var builder = new VariantBuilder (VariantType.STRING_ARRAY);
+ foreach (var player in this.players)
+ builder.add ("s", player.id);
+ this.settings.set_value ("interested-media-players", builder.end ());
+ }
}
void player_added (MediaPlayer player) {
diff --git a/src/sound-menu.vala b/src/sound-menu.vala
index 480e1cf..3fdfc36 100644
--- a/src/sound-menu.vala
+++ b/src/sound-menu.vala
@@ -22,7 +22,8 @@ class SoundMenu: Object
public enum DisplayFlags {
NONE = 0,
SHOW_MUTE = 1,
- HIDE_INACTIVE_PLAYERS = 2
+ HIDE_INACTIVE_PLAYERS = 2,
+ HIDE_PLAYERS = 4
}
public SoundMenu (string? settings_action, DisplayFlags flags) {
@@ -55,6 +56,7 @@ class SoundMenu: Object
this.root = new Menu ();
root.append_item (root_item);
+ this.hide_players = (flags & DisplayFlags.HIDE_PLAYERS) != 0;
this.hide_inactive = (flags & DisplayFlags.HIDE_INACTIVE_PLAYERS) != 0;
this.notify_handlers = new HashTable<MediaPlayer, ulong> (direct_hash, direct_equal);
}
@@ -119,6 +121,7 @@ class SoundMenu: Object
bool mic_volume_shown;
bool settings_shown = false;
bool hide_inactive;
+ bool hide_players = false;
HashTable<MediaPlayer, ulong> notify_handlers;
/* returns the position in this.menu of the section that's associated with @player */
@@ -137,6 +140,9 @@ class SoundMenu: Object
}
void insert_player_section (MediaPlayer player) {
+ if (this.hide_players)
+ return;
+
var section = new Menu ();
Icon icon;
@@ -166,6 +172,9 @@ class SoundMenu: Object
}
void remove_player_section (MediaPlayer player) {
+ if (this.hide_players)
+ return;
+
int index = this.find_player_section (player);
if (index >= 0)
this.menu.remove (index);
diff --git a/src/volume-control.vala b/src/volume-control.vala
index e994922..03cac0b 100644
--- a/src/volume-control.vala
+++ b/src/volume-control.vala
@@ -31,6 +31,7 @@ public class VolumeControl : Object
private PulseAudio.Context context;
private bool _mute = true;
+ private bool _is_playing = false;
private double _volume = 0.0;
private double _mic_volume = 0.0;
@@ -55,6 +56,7 @@ public class VolumeControl : Object
{
if (_reconnect_timer != 0) {
Source.remove (_reconnect_timer);
+ _reconnect_timer = 0;
}
}
@@ -97,6 +99,13 @@ public class VolumeControl : Object
this.notify_property ("mute");
}
+ var playing = (i.state == PulseAudio.SinkState.RUNNING);
+ if (_is_playing != playing)
+ {
+ _is_playing = playing;
+ this.notify_property ("is-playing");
+ }
+
if (_volume != volume_to_double (i.volume.values[0]))
{
_volume = volume_to_double (i.volume.values[0]);
@@ -235,6 +244,14 @@ public class VolumeControl : Object
}
}
+ public bool is_playing
+ {
+ get
+ {
+ return this._is_playing;
+ }
+ }
+
/* Volume operations */
private static PulseAudio.Volume double_to_volume (double vol)
{
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 8e79fd0..1556fc7 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -10,6 +10,77 @@ add_library (gtest STATIC
${GTEST_SOURCE_DIR}/gtest_main.cc)
target_link_libraries(gtest ${GTEST_LIBS})
+###########################
+# Vala Mocks
+###########################
+
+set(VALA_MOCKS_HEADER_PATH "${CMAKE_CURRENT_BINARY_DIR}/vala-mocks.h")
+set(VALA_MOCKS_SYMBOLS_PATH "${CMAKE_CURRENT_BINARY_DIR}/vala-mocks.def")
+
+vala_init(vala-mocks
+ DEPENDS
+ indicator-sound-service-lib
+ PACKAGES
+ config
+ gio-2.0
+ gio-unix-2.0
+ libxml-2.0
+ libpulse
+ libpulse-mainloop-glib
+ libnotify
+ accounts-service
+ indicator-sound-service
+ OPTIONS
+ --ccode
+ --thread
+ --vapidir=${CMAKE_BINARY_DIR}/src/
+ --vapidir=${CMAKE_SOURCE_DIR}/vapi/
+ --vapidir=.
+)
+
+vala_add(vala-mocks
+ media-player-mock.vala
+)
+
+vala_finish(vala-mocks
+ SOURCES
+ vala_mocks_VALA_SOURCES
+ OUTPUTS
+ vala_mocks_VALA_C
+ GENERATE_HEADER
+ ${VALA_MOCKS_HEADER_PATH}
+ GENERATE_SYMBOLS
+ ${VALA_MOCKS_SYMBOLS_PATH}
+)
+
+set_source_files_properties(
+ ${vala_mocks_VALA_SOURCES}
+ PROPERTIES
+ HEADER_FILE_ONLY TRUE
+)
+
+set(
+ VALA_MOCKS_SOURCES
+ ${vala_mocks_VALA_SOURCES}
+ ${vala_mocks_VALA_C}
+ ${VALA_MOCKS_SYMBOLS_PATH}
+)
+
+add_definitions(
+ -Wno-unused-but-set-variable
+)
+
+add_library(
+ vala-mocks-lib STATIC
+ ${VALA_MOCKS_SOURCES}
+)
+
+target_link_libraries(
+ vala-mocks-lib
+ indicator-sound-service-lib
+)
+
+include_directories(${CMAKE_CURRENT_BINARY_DIR})
###########################
# Name Watch Test
@@ -20,3 +91,26 @@ add_executable (name-watch-test name-watch-test.cc ${CMAKE_SOURCE_DIR}/src/bus-w
target_link_libraries (name-watch-test gtest ${SOUNDSERVICE_LIBRARIES})
add_test(name-watch-test name-watch-test)
+###########################
+# Accounts Service User
+###########################
+
+include_directories(${CMAKE_SOURCE_DIR}/src)
+add_executable (accounts-service-user-test accounts-service-user.cc)
+target_link_libraries (
+ accounts-service-user-test
+ indicator-sound-service-lib
+ vala-mocks-lib
+ gtest
+ ${SOUNDSERVICE_LIBRARIES}
+ ${TEST_LIBRARIES}
+)
+
+# Split tests to work around libaccountservice sucking
+add_test(accounts-service-user-test-basic
+ accounts-service-user-test --gtest_filter=AccountsServiceUserTest.BasicObject
+)
+
+add_test(accounts-service-user-test-player
+ accounts-service-user-test --gtest_filter=AccountsServiceUserTest.SetMediaPlayer
+)
diff --git a/tests/accounts-service-mock.h b/tests/accounts-service-mock.h
new file mode 100644
index 0000000..225d7b5
--- /dev/null
+++ b/tests/accounts-service-mock.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+#include <libdbustest/dbus-test.h>
+
+class AccountsServiceMock
+{
+ DbusTestDbusMock * mock = nullptr;
+
+ public:
+ AccountsServiceMock () {
+ mock = dbus_test_dbus_mock_new("org.freedesktop.Accounts");
+
+ DbusTestDbusMockObject * baseobj = dbus_test_dbus_mock_get_object(mock, "/org/freedesktop/Accounts", "org.freedesktop.Accounts", NULL);
+
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "CacheUser", G_VARIANT_TYPE_STRING, G_VARIANT_TYPE_OBJECT_PATH,
+ "ret = dbus.ObjectPath('/user')\n", NULL);
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "FindUserById", G_VARIANT_TYPE_INT64, G_VARIANT_TYPE_OBJECT_PATH,
+ "ret = dbus.ObjectPath('/user')\n", NULL);
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "FindUserByName", G_VARIANT_TYPE_STRING, G_VARIANT_TYPE_OBJECT_PATH,
+ "ret = dbus.ObjectPath('/user')\n", NULL);
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "ListCachedUsers", NULL, G_VARIANT_TYPE_OBJECT_PATH_ARRAY,
+ "ret = [ dbus.ObjectPath('/user') ]\n", NULL);
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "UncacheUser", G_VARIANT_TYPE_STRING, NULL,
+ "", NULL);
+
+ DbusTestDbusMockObject * userobj = dbus_test_dbus_mock_get_object(mock, "/user", "org.freedesktop.Accounts.User", NULL);
+ dbus_test_dbus_mock_object_add_property(mock, userobj,
+ "UserName", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(g_get_user_name()), NULL);
+
+ DbusTestDbusMockObject * soundobj = dbus_test_dbus_mock_get_object(mock, "/user", "com.canonical.indicator.sound.AccountsService", NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "Timestamp", G_VARIANT_TYPE_UINT64,
+ g_variant_new_uint64(0), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "PlayerName", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "PlayerIcon", G_VARIANT_TYPE_VARIANT,
+ g_variant_new_variant(g_variant_new_string("")), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "Running", G_VARIANT_TYPE_BOOLEAN,
+ g_variant_new_boolean(FALSE), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "State", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "Title", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "Artist", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "Album", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "ArtUrl", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+ }
+
+ ~AccountsServiceMock () {
+ g_clear_object(&mock);
+ }
+
+ operator DbusTestTask* () {
+ return DBUS_TEST_TASK(mock);
+ }
+};
diff --git a/tests/accounts-service-user.cc b/tests/accounts-service-user.cc
new file mode 100644
index 0000000..b39b546
--- /dev/null
+++ b/tests/accounts-service-user.cc
@@ -0,0 +1,201 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+#include <gtest/gtest.h>
+#include <gio/gio.h>
+#include <libdbustest/dbus-test.h>
+#include <act/act.h>
+
+#include "accounts-service-mock.h"
+
+extern "C" {
+#include "indicator-sound-service.h"
+#include "vala-mocks.h"
+}
+
+class AccountsServiceUserTest : public ::testing::Test
+{
+
+ protected:
+ DbusTestService * service = NULL;
+ DbusTestDbusMock * mock = NULL;
+
+ GDBusConnection * session = NULL;
+ GDBusConnection * system = NULL;
+ GDBusProxy * proxy = NULL;
+
+ virtual void SetUp() {
+ service = dbus_test_service_new(NULL);
+
+ AccountsServiceMock service_mock;
+
+ dbus_test_service_add_task(service, (DbusTestTask*)service_mock);
+ dbus_test_service_start_tasks(service);
+
+ g_setenv("DBUS_SYSTEM_BUS_ADDRESS", g_getenv("DBUS_SESSION_BUS_ADDRESS"), TRUE);
+
+ session = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
+ ASSERT_NE(nullptr, session);
+ g_dbus_connection_set_exit_on_close(session, FALSE);
+ g_object_add_weak_pointer(G_OBJECT(session), (gpointer *)&session);
+
+ system = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL);
+ ASSERT_NE(nullptr, system);
+ g_dbus_connection_set_exit_on_close(system, FALSE);
+ g_object_add_weak_pointer(G_OBJECT(system), (gpointer *)&system);
+
+ proxy = g_dbus_proxy_new_sync(session,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.freedesktop.Accounts",
+ "/user",
+ "org.freedesktop.DBus.Properties",
+ NULL, NULL);
+ ASSERT_NE(nullptr, proxy);
+ }
+
+ virtual void TearDown() {
+ g_clear_object(&proxy);
+ g_clear_object(&mock);
+ g_clear_object(&service);
+
+ g_object_unref(session);
+ g_object_unref(system);
+
+ #if 0
+ /* Accounts Service keeps a bunch of references around so we
+ have to split the tests and can't check this :-( */
+ unsigned int cleartry = 0;
+ while ((session != NULL || system != NULL) && cleartry < 100) {
+ loop(100);
+ cleartry++;
+ }
+
+ ASSERT_EQ(nullptr, session);
+ ASSERT_EQ(nullptr, system);
+ #endif
+ }
+
+ static gboolean timeout_cb (gpointer user_data) {
+ GMainLoop * loop = static_cast<GMainLoop *>(user_data);
+ g_main_loop_quit(loop);
+ return G_SOURCE_REMOVE;
+ }
+
+ void loop (unsigned int ms) {
+ GMainLoop * loop = g_main_loop_new(NULL, FALSE);
+ g_timeout_add(ms, timeout_cb, loop);
+ g_main_loop_run(loop);
+ g_main_loop_unref(loop);
+ }
+
+ static int unref_idle (gpointer user_data) {
+ g_variant_unref(static_cast<GVariant *>(user_data));
+ return G_SOURCE_REMOVE;
+ }
+
+ const gchar * get_property_string (const gchar * name) {
+ GVariant * propval = g_dbus_proxy_call_sync(proxy,
+ "Get",
+ g_variant_new("(ss)", "com.canonical.indicator.sound.AccountsService", name),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, NULL
+ );
+
+ if (propval == nullptr) {
+ return nullptr;
+ }
+
+ /* NOTE: This is a bit of a hack, basically if main gets
+ called the returned string becomes invalid. But it
+ makes the test much easier to read :-/ */
+ g_idle_add(unref_idle, propval);
+
+ const gchar * ret = NULL;
+ GVariant * child = g_variant_get_child_value(propval, 0);
+ GVariant * vstr = g_variant_get_variant(child);
+ ret = g_variant_get_string(vstr, NULL);
+ g_variant_unref(vstr);
+ g_variant_unref(child);
+
+ return ret;
+ }
+};
+
+TEST_F(AccountsServiceUserTest, BasicObject) {
+ AccountsServiceUser * srv = accounts_service_user_new();
+ loop(50);
+ g_object_unref(srv);
+}
+
+TEST_F(AccountsServiceUserTest, SetMediaPlayer) {
+ MediaPlayerTrack * track = media_player_track_new("Artist", "Title", "Album", "http://art.url");
+
+ MediaPlayerMock * media = MEDIA_PLAYER_MOCK(
+ g_object_new(TYPE_MEDIA_PLAYER_MOCK,
+ "mock-id", "player-id",
+ "mock-name", "Test Player",
+ "mock-state", "Playing",
+ "mock-is-running", TRUE,
+ "mock-can-raise", FALSE,
+ "mock-current-track", track,
+ NULL)
+ );
+ g_clear_object(&track);
+
+ AccountsServiceUser * srv = accounts_service_user_new();
+
+ accounts_service_user_set_player(srv, MEDIA_PLAYER(media));
+
+ loop(500);
+
+ /* Verify the values are on the other side of the bus */
+ EXPECT_STREQ("Test Player", get_property_string("PlayerName"));
+ EXPECT_STREQ("Playing", get_property_string("State"));
+ EXPECT_STREQ("Title", get_property_string("Title"));
+ EXPECT_STREQ("Artist", get_property_string("Artist"));
+ EXPECT_STREQ("Album", get_property_string("Album"));
+ EXPECT_STREQ("http://art.url", get_property_string("ArtUrl"));
+
+ /* Check changing the track info */
+ track = media_player_track_new("Artist-ish", "Title-like", "Psuedo Album", "http://fake.art.url");
+ media_player_mock_set_mock_current_track(media, track);
+ g_clear_object(&track);
+ accounts_service_user_set_player(srv, MEDIA_PLAYER(media));
+
+ loop(500);
+
+ EXPECT_STREQ("Test Player", get_property_string("PlayerName"));
+ EXPECT_STREQ("Playing", get_property_string("State"));
+ EXPECT_STREQ("Title-like", get_property_string("Title"));
+ EXPECT_STREQ("Artist-ish", get_property_string("Artist"));
+ EXPECT_STREQ("Psuedo Album", get_property_string("Album"));
+ EXPECT_STREQ("http://fake.art.url", get_property_string("ArtUrl"));
+
+ /* Check to ensure the state can be updated */
+ media_player_set_state(MEDIA_PLAYER(media), "Paused");
+ accounts_service_user_set_player(srv, MEDIA_PLAYER(media));
+
+ loop(500);
+
+ EXPECT_STREQ("Paused", get_property_string("State"));
+
+ g_object_unref(media);
+ g_object_unref(srv);
+}
diff --git a/tests/manual b/tests/manual
new file mode 100644
index 0000000..201465c
--- /dev/null
+++ b/tests/manual
@@ -0,0 +1,24 @@
+
+Test-case indicator-sound/unity7-items-check
+<dl>
+ <dt>Log in to a Unity 7 user session</dt>
+ <dt>Go to the panel and click on the Sound indicator</dt>
+ <dd>Ensure there are items in the menu</dd>
+</dl>
+
+Test-case indicator-sound/unity7-greeter-items-check
+<dl>
+ <dt>Start a system and wait for the greeter or logout of the current user session</dt>
+ <dt>Go to the panel and click on the Sound indicator</dt>
+ <dd>Ensure there are items in the menu</dd>
+</dl>
+
+Test-case indicator-sound/unity8-items-check
+<dl>
+ <dt>Login to a user session running Unity 8</dt>
+ <dt>Pull down the top panel until it sticks open</dt>
+ <dt>Navigate through the tabs until "Sound" is shown</dt>
+ <dd>Sound is at the top of the menu</dd>
+ <dd>The menu is populated with items</dd>
+</dl>
+
diff --git a/tests/media-player-mock.vala b/tests/media-player-mock.vala
new file mode 100644
index 0000000..14028f5
--- /dev/null
+++ b/tests/media-player-mock.vala
@@ -0,0 +1,76 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+public class MediaPlayerMock: MediaPlayer {
+
+ /* Superclass variables */
+ public override string id { get { return mock_id; } }
+ public override string name { get { return mock_name; } }
+ public override string state { get { return mock_state; } set { this.mock_state = value; }}
+ public override Icon? icon { get { return mock_icon; } }
+ public override string dbus_name { get { return mock_dbus_name; } }
+
+ public override bool is_running { get { return mock_is_running; } }
+ public override bool can_raise { get { return mock_can_raise; } }
+
+ public override MediaPlayer.Track? current_track { get { return mock_current_track; } set { this.mock_current_track = value; } }
+
+ /* Mock values */
+ public string mock_id { get; set; }
+ public string mock_name { get; set; }
+ public string mock_state { get; set; }
+ public Icon? mock_icon { get; set; }
+ public string mock_dbus_name { get; set; }
+
+ public bool mock_is_running { get; set; }
+ public bool mock_can_raise { get; set; }
+
+ public MediaPlayer.Track? mock_current_track { get; set; }
+
+ /* Virtual functions */
+ public override void activate () {
+ debug("Mock activate");
+ }
+ public override void play_pause () {
+ debug("Mock play_pause");
+ }
+ public override void next () {
+ debug("Mock next");
+ }
+ public override void previous () {
+ debug("Mock previous");
+ }
+
+ public override uint get_n_playlists() {
+ debug("Mock get_n_playlists");
+ return 0;
+ }
+ public override string get_playlist_id (int index) {
+ debug("Mock get_playlist_id");
+ return "";
+ }
+ public override string get_playlist_name (int index) {
+ debug("Mock get_playlist_name");
+ return "";
+ }
+ public override void activate_playlist_by_name (string playlist) {
+ debug("Mock activate_playlist_by_name");
+ }
+
+}