diff options
-rw-r--r-- | .bzrignore | 10 | ||||
-rw-r--r-- | CMakeLists.txt | 8 | ||||
-rw-r--r-- | data/50-com.canonical.indicator.sound.AccountsService.pkla | 6 | ||||
-rw-r--r-- | data/CMakeLists.txt | 32 | ||||
-rw-r--r-- | data/com.canonical.indicator.sound.AccountsService.policy | 23 | ||||
-rw-r--r-- | data/com.canonical.indicator.sound.AccountsService.xml | 42 | ||||
-rw-r--r-- | data/com.canonical.indicator.sound.gschema.xml | 9 | ||||
-rw-r--r-- | debian/control | 6 | ||||
-rwxr-xr-x | debian/rules | 5 | ||||
-rw-r--r-- | src/CMakeLists.txt | 61 | ||||
-rw-r--r-- | src/accounts-service-sound-settings.vala | 33 | ||||
-rw-r--r-- | src/accounts-service-user.vala | 135 | ||||
-rw-r--r-- | src/main.c | 32 | ||||
-rw-r--r-- | src/main.vala | 14 | ||||
-rw-r--r-- | src/media-player-list.vala | 20 | ||||
-rw-r--r-- | src/media-player-mpris.vala | 299 | ||||
-rw-r--r-- | src/media-player.vala | 297 | ||||
-rw-r--r-- | src/service.vala | 60 | ||||
-rw-r--r-- | src/volume-control.vala | 1 | ||||
-rw-r--r-- | tests/CMakeLists.txt | 94 | ||||
-rw-r--r-- | tests/accounts-service-mock.h | 90 | ||||
-rw-r--r-- | tests/accounts-service-user.cc | 201 | ||||
-rw-r--r-- | tests/media-player-mock.vala | 76 |
23 files changed, 1246 insertions, 308 deletions
@@ -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/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 80046f7..955a4a4 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -82,3 +82,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.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/control b/debian/control index c7b3ed8..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 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 d4a5bc6..f6c5f01 100644 --- a/src/service.vala +++ b/src/service.vala @@ -46,6 +46,10 @@ public class IndicatorSound.Service: Object { this.volume_control.bind_property ("active-mic", menu, "show-mic-volume", BindingFlags.SYNC_CREATE); }); + /* 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.sync_preferred_players (); @@ -62,6 +66,41 @@ public class IndicatorSound.Service: Object { 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 () { if (this.loop != null) { warning ("service is already running"); @@ -72,8 +111,17 @@ public class IndicatorSound.Service: Object { 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; } @@ -117,6 +165,7 @@ public class IndicatorSound.Service: Object { 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(). @@ -343,14 +392,25 @@ public class IndicatorSound.Service: Object { } 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; } diff --git a/src/volume-control.vala b/src/volume-control.vala index 4347ce5..889c2d6 100644 --- a/src/volume-control.vala +++ b/src/volume-control.vala @@ -73,6 +73,7 @@ public class VolumeControl : Object { if (_reconnect_timer != 0) { Source.remove (_reconnect_timer); + _reconnect_timer = 0; } } 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/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"); + } + +} |