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-2019 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 

7 

8from xpra.util import ellipsizer, csv 

9from xpra.os_util import bytestostr 

10from xpra.dbus.helper import native_to_dbus 

11from xpra.notifications.notifier_base import NotifierBase, log 

12try: 

13 #new recommended way of using the glib main loop: 

14 from dbus.mainloop.glib import DBusGMainLoop 

15 DBusGMainLoop(set_as_default=True) 

16except ImportError: 

17 #beware: this import has side-effects: 

18 import dbus.glib 

19 assert dbus.glib 

20import dbus.exceptions 

21 

22NOTIFICATION_APP_NAME = os.environ.get("XPRA_NOTIFICATION_APP_NAME", "%s (via Xpra)") 

23 

24 

25def DBUS_Notifier_factory(*args): 

26 try: 

27 return DBUS_Notifier(*args) 

28 except Exception as e: 

29 log.warn("failed to instantiate the dbus notification handler:") 

30 if str(e).startswith("org.freedesktop.DBus.Error.ServiceUnknown:"): 

31 log.warn(" you may need to start a notification service for 'org.freedesktop.Notifications'") 

32 else: 

33 log.warn(" %s", e) 

34 log.warn(" disable notifications to avoid this warning") 

35 return None 

36 

37class DBUS_Notifier(NotifierBase): 

38 

39 def __init__(self, *args): 

40 super().__init__(*args) 

41 self.app_name_format = NOTIFICATION_APP_NAME 

42 self.last_notification = None 

43 self.actual_notification_id = {} 

44 self.setup_dbusnotify() 

45 self.handles_actions = True 

46 

47 def setup_dbusnotify(self): 

48 self.dbus_session = dbus.SessionBus() 

49 FD_NOTIFICATIONS = 'org.freedesktop.Notifications' 

50 self.org_fd_notifications = self.dbus_session.get_object(FD_NOTIFICATIONS, '/org/freedesktop/Notifications') 

51 self.org_fd_notifications.connect_to_signal("NotificationClosed", self.NotificationClosed) 

52 self.org_fd_notifications.connect_to_signal("ActionInvoked", self.ActionInvoked) 

53 

54 #connect_to_signal("HelloSignal", hello_signal_handler, dbus_interface="com.example.TestService", arg0="Hello") 

55 self.dbusnotify = dbus.Interface(self.org_fd_notifications, FD_NOTIFICATIONS) 

56 log("using dbusnotify: %s(%s)", type(self.dbusnotify), FD_NOTIFICATIONS) 

57 caps = tuple(str(x) for x in self.dbusnotify.GetCapabilities()) 

58 log("capabilities=%s", csv(caps)) 

59 self.handles_actions = "actions" in caps 

60 log("dbus.get_default_main_loop()=%s", dbus.get_default_main_loop()) 

61 

62 def cleanup(self): 

63 nids = list(self.actual_notification_id.items()) 

64 self.actual_notification_id = {} 

65 for nid, actual_id in nids: 

66 self.do_close(nid, actual_id) 

67 NotifierBase.cleanup(self) 

68 

69 

70 def show_notify(self, dbus_id, tray, nid, 

71 app_name, replaces_nid, app_icon, 

72 summary, body, actions, hints, timeout, icon): 

73 if not self.dbus_check(dbus_id): 

74 return 

75 self.may_retry = True 

76 try: 

77 icon_string = self.get_icon_string(nid, app_icon, icon) 

78 log("get_icon_string%s=%s", (nid, app_icon, ellipsizer(icon)), icon_string) 

79 if app_name=="Xpra": 

80 #don't show "Xpra (via Xpra)" 

81 app_str = "Xpra" 

82 else: 

83 try: 

84 app_str = self.app_name_format % app_name 

85 except TypeError: 

86 app_str = app_name or "Xpra" 

87 self.last_notification = ( 

88 dbus_id, tray, nid, app_name, replaces_nid, 

89 app_icon, summary, body, timeout, icon, 

90 ) 

91 def NotifyReply(notification_id): 

92 log("NotifyReply(%s) for nid=%i", notification_id, nid) 

93 self.actual_notification_id[nid] = int(notification_id) 

94 dbus_hints = self.parse_hints(hints) 

95 log("calling %s%s", self.dbusnotify.Notify, 

96 (app_str, 0, icon_string, summary, body, actions, dbus_hints, timeout)) 

97 self.dbusnotify.Notify(app_str, 0, icon_string, summary, body, actions, dbus_hints, timeout, 

98 reply_handler = NotifyReply, 

99 error_handler = self.NotifyError) 

100 except Exception: 

101 log.error("Error: dbus notify failed", exc_info=True) 

102 

103 def _find_nid(self, actual_id : int): 

104 aid = int(actual_id) 

105 for k,v in self.actual_notification_id.items(): 

106 if v==aid: 

107 return k 

108 return None 

109 

110 def noparse_hints(self, h) -> dict: 

111 return h 

112 

113 def parse_hints(self, h) -> dict: 

114 hints = {} 

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

116 v = h.get(x) 

117 if v is not None: 

118 hints[x] = native_to_dbus(v) 

119 image_data = h.get("image-data") 

120 if image_data and bytestostr(image_data[0])=="png": 

121 try: 

122 from xpra.codecs.pillow.decoder import open_only 

123 img_data = image_data[3] 

124 img = open_only(img_data, ("png",)) 

125 w, h = img.size 

126 channels = len(img.mode) 

127 rowstride = w*channels 

128 has_alpha = img.mode=="RGBA" 

129 pixel_data = bytearray(img.tobytes("raw", img.mode)) 

130 log.info("pixel_data=%s", type(pixel_data)) 

131 args = w, h, rowstride, has_alpha, 8, channels, pixel_data 

132 hints["image-data"] = tuple(native_to_dbus(x) for x in args) 

133 #hints["image-data"] = args 

134 except Exception as e: 

135 log("parse_hints(%s) error on image-data=%s", h, image_data, exc_info=True) 

136 log.error("Error parsing notification image:") 

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

138 log("parse_hints(%s)=%s", h, hints) 

139 #return dbus.types.Dictionary(hints, signature="sv") 

140 return hints 

141 

142 

143 def NotificationClosed(self, actual_id : int, reason): 

144 nid = self._find_nid(actual_id) 

145 reason_str = { 

146 1 : "expired", 

147 2 : "dismissed by the user", 

148 3 : "closed by a call to CloseNotification", 

149 4 : "Undefined/reserved reasons", 

150 }.get(int(reason), str(reason)) 

151 log("NotificationClosed(%s, %s) nid=%s, reason=%s", actual_id, reason, nid, reason_str) 

152 if nid: 

153 self.actual_notification_id.pop(nid, None) 

154 self.clean_notification(nid) 

155 if self.closed_cb: 

156 self.closed_cb(nid, int(reason), reason_str) 

157 

158 def ActionInvoked(self, actual_id : int, action): 

159 nid = self._find_nid(actual_id) 

160 log("ActionInvoked(%s, %s) nid=%s", actual_id, action, nid) 

161 if nid: 

162 if self.action_cb: 

163 self.action_cb(nid, str(action)) 

164 

165 def NotifyError(self, dbus_error, *_args): 

166 try: 

167 if isinstance(dbus_error, dbus.exceptions.DBusException): 

168 message = dbus_error.get_dbus_message() 

169 dbus_error_name = dbus_error.get_dbus_name() 

170 if dbus_error_name!="org.freedesktop.DBus.Error.ServiceUnknown": 

171 log.error("unhandled dbus exception: %s, %s", message, dbus_error_name) 

172 return False 

173 

174 if not self.may_retry: 

175 log.error("Error: cannot send notification via dbus,") 

176 log.error(" check that you notification service is operating properly") 

177 return False 

178 self.may_retry = False 

179 

180 log.info("trying to re-connect to the notification service") 

181 #try to connect to the notification again (just once): 

182 self.setup_dbusnotify() 

183 #and retry: 

184 self.show_notify(*self.last_notification) 

185 except Exception: 

186 log("cannot filter error", exc_info=True) 

187 log.error("Error processing notification:") 

188 log.error(" %s", dbus_error) 

189 return False 

190 

191 def close_notify(self, nid : int): 

192 actual_id = self.actual_notification_id.get(nid) 

193 if actual_id is None: 

194 log("close_notify(%i) actual notification not found, already closed?", nid) 

195 return 

196 log("close_notify(%i) actual id=%s", nid, actual_id) 

197 self.do_close(nid, actual_id) 

198 

199 def do_close(self, nid : int, actual_id : int): 

200 log("do_close_notify(%i)", actual_id) 

201 def CloseNotificationReply(): 

202 self.actual_notification_id.pop(nid, None) 

203 def CloseNotificationError(dbus_error, *_args): 

204 log.warn("Error: error closing notification:") 

205 log.warn(" %s", dbus_error) 

206 self.dbusnotify.CloseNotification(actual_id, 

207 reply_handler = CloseNotificationReply, 

208 error_handler = CloseNotificationError) 

209 

210 

211def main(): 

212 from gi.repository import GLib, Gtk 

213 def show(): 

214 n = DBUS_Notifier_factory() 

215 #actions = ["0", "Hello", "1", "Bye"] 

216 actions = [] 

217 n.show_notify("", None, 0, "Test", 0, "", "Summary", "Body line1\nline2...", 

218 actions, {}, 0, "") 

219 return False 

220 GLib.idle_add(show) 

221 GLib.timeout_add(20000, Gtk.main_quit) 

222 Gtk.main() 

223 

224 

225if __name__ == "__main__": 

226 main()