Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of Xpra. 

2# Copyright (C) 2011-2018 Antoine Martin <antoine@xpra.org> 

3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 

4# later version. See the file COPYING for details. 

5 

6import os 

7import dbus.service 

8 

9from xpra.notifications.common import parse_image_data, parse_image_path 

10from xpra.dbus.helper import dbus_to_native 

11from xpra.util import envbool, csv 

12from xpra.log import Logger 

13 

14log = Logger("dbus", "notify") 

15 

16BUS_NAME="org.freedesktop.Notifications" 

17BUS_PATH="/org/freedesktop/Notifications" 

18 

19ACTIONS = envbool("XPRA_NOTIFICATIONS_ACTIONS", True) 

20 

21 

22""" 

23We register this class as handling notifications on the session dbus, 

24optionally replacing an existing instance if one exists. 

25 

26The generalized callback signatures are: 

27 notify_callback(dbus_id, nid, app_name, replaces_nid, app_icon, summary, body, actions, hints, expire_timeout) 

28 close_callback(nid) 

29""" 

30class DBUSNotificationsForwarder(dbus.service.Object): 

31 

32 def __init__(self, bus, notify_callback=None, close_callback=None): 

33 self.bus = bus 

34 self.notify_callback = notify_callback 

35 self.close_callback = close_callback 

36 self.active_notifications = set() 

37 self.counter = 0 

38 self.support_actions = True 

39 self.dbus_id = os.environ.get("DBUS_SESSION_BUS_ADDRESS", "") 

40 bus_name = dbus.service.BusName(BUS_NAME, bus=bus) 

41 super().__init__(bus_name, BUS_PATH) 

42 

43 def get_info(self) -> dict: 

44 return { 

45 "active" : tuple(self.active_notifications), 

46 "counter" : self.counter, 

47 "actions" : self.support_actions, 

48 "dbus-id" : self.dbus_id, 

49 "bus-name" : BUS_NAME, 

50 "bus-path" : BUS_PATH, 

51 "capabilities" : self.do_get_capabilities(), 

52 } 

53 

54 def next_id(self): 

55 self.counter += 1 

56 return self.counter 

57 

58 @dbus.service.method(BUS_NAME, in_signature='susssasa{sv}i', out_signature='u') 

59 def Notify(self, app_name, replaces_nid, app_icon, summary, body, actions, hints, expire_timeout): 

60 if replaces_nid==0: 

61 nid = self.next_id() 

62 else: 

63 nid = int(replaces_nid) 

64 log("Notify%s nid=%s, counter=%i, callback=%s", 

65 (app_name, replaces_nid, app_icon, summary, body, actions, hints, expire_timeout), 

66 nid, self.counter, self.notify_callback) 

67 self.active_notifications.add(nid) 

68 if self.notify_callback: 

69 try: 

70 actions = tuple(str(x) for x in actions) 

71 hints = self.parse_hints(hints) 

72 args = ( 

73 self.dbus_id, int(nid), str(app_name), 

74 int(replaces_nid), str(app_icon), 

75 str(summary), str(body), 

76 actions, hints, int(expire_timeout), 

77 ) 

78 except Exception as e: 

79 log.error("Error: failed to parse Notify arguments:") 

80 log.error(" %s", e) 

81 try: 

82 self.notify_callback(*args) 

83 except Exception as e: 

84 log.error("Error calling notification handler", exc_info=True) 

85 log("Notify returning %s", nid) 

86 return nid 

87 

88 def parse_hints(self, dbus_hints): 

89 hints = {} 

90 h = dbus_to_native(dbus_hints) 

91 for x in ("image-data", "icon_data"): 

92 data = h.pop(x, None) 

93 if data: 

94 v = parse_image_data(data) 

95 if v: 

96 hints["image-data"] = v 

97 break 

98 if "image-data" not in hints: 

99 image_path = h.pop("image-path", None) 

100 if image_path: 

101 v = parse_image_path(image_path) 

102 if v: 

103 hints["image-data"] = v 

104 for x in ("action-icons", "category", "desktop-entry", "resident", "transient", "x", "y", "urgency"): 

105 v = h.get(x) 

106 if v is not None: 

107 hints[x] = v 

108 log("parse_hints(%s)=%s", dbus_hints, hints) 

109 return hints 

110 

111 

112 @dbus.service.method(BUS_NAME, out_signature='ssss') 

113 def GetServerInformation(self): 

114 #name, vendor, version, spec-version 

115 from xpra import __version__ 

116 v = ["xpra-notification-proxy", "xpra", __version__, "0.9"] 

117 log("GetServerInformation()=%s", v) 

118 return v 

119 

120 @dbus.service.method(BUS_NAME, out_signature='as') 

121 def GetCapabilities(self): 

122 caps = self.do_get_capabilities() 

123 log("GetCapabilities()=%s", csv(caps)) 

124 return caps 

125 

126 def do_get_capabilities(self): 

127 caps = ["body", "icon-static"] 

128 if ACTIONS and self.support_actions: 

129 caps += ["actions", "action-icons"] 

130 return caps 

131 

132 @dbus.service.method(BUS_NAME, in_signature='u') 

133 def CloseNotification(self, nid): 

134 log("CloseNotification(%s) callback=%s", nid, self.close_callback) 

135 try: 

136 self.active_notifications.remove(int(nid)) 

137 except KeyError: 

138 return 

139 else: 

140 if self.close_callback: 

141 self.close_callback(nid) 

142 self.NotificationClosed(nid, 3) #3="The notification was closed by a call to CloseNotification" 

143 

144 def is_notification_active(self, nid): 

145 return nid in self.active_notifications 

146 

147 @dbus.service.signal(BUS_NAME, signature='uu') 

148 def NotificationClosed(self, nid, reason): 

149 pass 

150 

151 @dbus.service.signal(BUS_NAME, signature='us') 

152 def ActionInvoked(self, nid, action_key): 

153 pass 

154 

155 

156 def release(self): 

157 try: 

158 self.bus.release_name(BUS_NAME) 

159 except dbus.exceptions.DBusException as e: 

160 log("release()", exc_info=True) 

161 log.error("Error releasing the dbus notification forwarder:") 

162 for x in str(e).split(": "): 

163 log.error(" %s", x) 

164 

165 def __str__(self): 

166 return "DBUS-NotificationsForwarder(%s)" % BUS_NAME 

167 

168 

169def register(notify_callback=None, close_callback=None, replace=False): 

170 from xpra.dbus.common import init_session_bus 

171 bus = init_session_bus() 

172 flags = dbus.bus.NAME_FLAG_DO_NOT_QUEUE 

173 if replace: 

174 flags |= dbus.bus.NAME_FLAG_REPLACE_EXISTING 

175 request = bus.request_name(BUS_NAME, flags) 

176 log("notifications: bus name '%s', request=%s" % (BUS_NAME, request)) 

177 if request==dbus.bus.REQUEST_NAME_REPLY_EXISTS: 

178 raise Exception("the name '%s' is already claimed on the session bus" % BUS_NAME) 

179 return DBUSNotificationsForwarder(bus, notify_callback, close_callback) 

180 

181 

182def main(): 

183 register() 

184 from gi.repository import GLib 

185 mainloop = GLib.MainLoop() 

186 mainloop.run() 

187 

188if __name__ == "__main__": 

189 main()