1# Copyright 2014 Christoph Reiter
2#           2018 Ludovic Druette
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9import time
10
11from gi.repository import GLib, Gio
12
13from quodlibet.util import print_exc
14from ._base import MMKeysBackend, MMKeysAction
15
16
17def dbus_has_interface(dbus_name, dbus_path, dbus_interface):
18    try:
19        proxy = Gio.DBusProxy.new_for_bus_sync(
20            Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None,
21            dbus_name, dbus_path,
22            "org.freedesktop.DBus.Introspectable", None)
23        xml = proxy.Introspect()
24        node = Gio.DBusNodeInfo.new_for_xml(xml)
25        for iface in node.interfaces:
26            if iface.name == dbus_interface:
27                return True
28        return False
29    except GLib.Error:
30        return False
31
32
33class GnomeBackend(MMKeysBackend):
34
35    DBUS_NAME = "org.gnome.SettingsDaemon.MediaKeys"
36    DBUS_PATH = "/org/gnome/SettingsDaemon/MediaKeys"
37    DBUS_IFACE = "org.gnome.SettingsDaemon.MediaKeys"
38
39    _EVENTS = {
40        "Next": MMKeysAction.NEXT,
41        "Previous": MMKeysAction.PREV,
42        "Play": MMKeysAction.PLAYPAUSE,
43        "Pause": MMKeysAction.PAUSE,
44        "Stop": MMKeysAction.STOP,
45        "FastForward": MMKeysAction.FORWARD,
46        "Rewind": MMKeysAction.REWIND,
47        "Repeat": MMKeysAction.REPEAT,
48        "Shuffle": MMKeysAction.SHUFFLE
49    }
50
51    def __init__(self, name, callback):
52        self.__interface = None
53        self.__watch = None
54        self.__grab_time = -1
55        self.__name = name
56        self.__key_pressed_sig = None
57        self.__callback = callback
58        self.__enable_watch()
59
60    @classmethod
61    def is_active(cls):
62        """If the gsd plugin is active atm"""
63
64        return dbus_has_interface(cls.DBUS_NAME, cls.DBUS_PATH, cls.DBUS_IFACE)
65
66    def cancel(self):
67        if self.__callback:
68            self.__disable_watch()
69            self.__release()
70            self.__callback = None
71
72    def grab(self, update=True):
73        """Tells gsd that QL started or got the focus.
74        update: whether to send the current time or the last one"""
75
76        if update:
77            # so this breaks every 50 days.. ok..
78            self.__grab_time = int((time.time() * 1000)) & 0xFFFFFFFF
79        elif self.__grab_time < 0:
80            # can not send the last event if there was none
81            return
82
83        iface = self.__update_interface()
84        if not iface:
85            return
86
87        try:
88            iface.GrabMediaPlayerKeys('(su)', self.__name, self.__grab_time)
89        except GLib.Error:
90            print_exc()
91
92    def __update_interface(self):
93        """If __interface is None, set a proxy interface object and connect
94        to the key pressed signal."""
95
96        if self.__interface:
97            return self.__interface
98
99        try:
100            iface = Gio.DBusProxy.new_for_bus_sync(
101                Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None,
102                self.DBUS_NAME, self.DBUS_PATH, self.DBUS_IFACE, None)
103        except GLib.Error:
104            print_exc()
105        else:
106            self.__key_pressed_sig = iface.connect(
107                'g-signal', self.__on_signal)
108            self.__interface = iface
109
110        return self.__interface
111
112    def __enable_watch(self):
113        """Enable events for dbus name owner change"""
114        if self.__watch:
115            return
116
117        # This also triggers for existing name owners
118        self.__watch = Gio.bus_watch_name(
119            Gio.BusType.SESSION, self.DBUS_NAME, Gio.BusNameWatcherFlags.NONE,
120            self.__owner_appeared, self.__owner_vanished)
121
122    def __disable_watch(self):
123        """Disable name owner change events"""
124        if self.__watch:
125            Gio.bus_unwatch_name(self.__watch)
126            self.__watch = None
127
128    def __owner_appeared(self, bus, name, owner):
129        """This gets called when the owner of the dbus name appears
130        so we can handle gnome-settings-daemon restarts."""
131
132        if not self.__interface:
133            # new owner, get a new interface object and
134            # resend the last grab event
135            self.grab(update=False)
136
137    def __owner_vanished(self, bus, owner):
138        """This gets called when the owner of the dbus name disappears
139        so we can handle gnome-settings-daemon restarts."""
140
141        # owner gone, remove the signal matches/interface etc.
142        self.__release()
143
144    def __on_signal(self, proxy, sender, signal, args):
145        if signal == 'MediaPlayerKeyPressed':
146            application, action = tuple(args)[:2]
147            self.__key_pressed(application, action)
148
149    def __key_pressed(self, application, action):
150        if application != self.__name:
151            return
152
153        if action in self._EVENTS:
154            self.__callback(self._EVENTS[action])
155
156    def __release(self):
157        """Tells gsd that we don't want events anymore and
158        removes all signal matches"""
159
160        if not self.__interface:
161            return
162
163        if self.__key_pressed_sig:
164            self.__interface.disconnect(self.__key_pressed_sig)
165            self.__key_pressed_sig = None
166
167        try:
168            self.__interface.ReleaseMediaPlayerKeys('(s)', self.__name)
169        except GLib.Error:
170            print_exc()
171        self.__interface = None
172
173
174# https://mail.gnome.org/archives/desktop-devel-list/2017-April/msg00069.html
175class GnomeBackendOldName(GnomeBackend):
176    DBUS_NAME = "org.gnome.SettingsDaemon"
177
178
179class MateBackend(GnomeBackend):
180
181    DBUS_NAME = "org.mate.SettingsDaemon"
182    DBUS_PATH = "/org/mate/SettingsDaemon/MediaKeys"
183    DBUS_IFACE = "org.mate.SettingsDaemon.MediaKeys"
184