5
updated: March 26, 2011
7
Monitors DBUS for org.freedesktop.Notifications.Notify messages, parses them,
8
and notifies listeners when they arrive.
12
from gi.repository import GObject, Gtk
15
from dbus.mainloop.glib import DBusGMainLoop
20
logger = logging.getLogger("Notification")
22
class ImageDataException(Exception):
25
class ImageData(object):
26
"""Parses the image_data hint from a DBUS message."""
27
def __init__(self, image_data):
28
if len(image_data) < 7:
29
raise ImageDataException("Invalid image_data: " + repr(image_data))
31
self.width = int(image_data[0])
32
self.height = int(image_data[1])
33
self.rowstride = int(image_data[2])
34
self.has_alpha = bool(image_data[3])
35
self.bits_per_sample = int(image_data[4])
36
self.channels = int(image_data[5])
37
self.data = self.dbus_array_to_str(image_data[6])
39
def dbus_array_to_str(self, array):
40
return "".join(map(chr, array))
42
def get_pixbuf(self, size):
43
"""Creates a pixbuf from the image data and scale it to the appropriate size."""
44
pixbuf = GdkPixbuf.Pixbuf.new_from_data(self.data, GdkPixbuf.Colorspace.RGB,
45
self.has_alpha, self.bits_per_sample, self.width, self.height,
48
return pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.BILINEAR)
50
class MessageException(Exception):
53
class Message(GObject.GObject):
54
"""Parses a DBUS message in the Notify format specified at:
55
http://www.galago-project.org/specs/notification/0.9/index.html"""
60
X_CANONICAL_PRIVATE_SYNCHRONOUS = "x-canonical-private-synchronous"
61
def __init__(self, dbus_message = None, timestamp = None):
62
GObject.GObject.__init__(self)
64
self.timestamp = timestamp
66
args = dbus_message.get_args_list()
69
raise MessageException("Invalid message args_list: " + repr(args))
71
self.app_name = str(args[0])
72
self.replaces_id = args[1]
73
self.app_icon = str(args[2])
74
self.summary = str(args[3])
75
self.body = str(args[4])
76
self.actions = args[5]
77
self.hints = dict(args[6])
78
self.expire_timeout = args[7]
80
if "urgency" in self.hints:
81
urgency = self.hints["urgency"]
83
self.urgency = Message.LOW
85
self.urgency = Message.CRITICAL
87
self.urgency = Message.NORMAL
89
self.urgency = Message.NORMAL
91
if "image_data" in self.hints:
92
self.image_data = ImageData(self.hints["image_data"])
94
self.image_data = None
96
if self.image_data == None and "icon_data" in self.hints:
97
self.image_data = ImageData(self.hints["icon_data"])
101
def get_icon(self, size = 48):
102
"""Loads the icon into a pixbuf. Adapted from the load_icon code in
103
bubble.c of notify-osd."""
104
icon_name = self.app_icon
106
# Try to load the pixbuf from a file
107
if icon_name.startswith("file://") or icon_name.startswith("/"):
108
icon = Icon.load_from_file(icon_name, size)
112
# Try to load the pixbuf from the current icon theme
113
elif icon_name != "":
114
icon = Icon.load(icon_name, size)
118
# Try to load the image data from the message
119
elif self.image_data != None:
120
return self.image_data.get_pixbuf(32)
122
return self.get_default_icon(size)
124
def get_default_icon(self, size = 48):
125
"""Attempts to load the default message icon, returns None on failure."""
126
if self.urgency == Message.LOW:
127
return Icon.load("notification-low", size)
128
elif self.urgency == Message.CRITICAL:
129
return Icon.load("notification-critical", size)
131
return Icon.load("notification-normal", size)
133
def is_volume_notification(self):
134
"""Returns true if this is a volume message. The volume notifications
135
in Ubuntu are a special case, and mostly a blank message. Clutters up
136
the display and provides no useful information, so it is better to
138
if Message.X_CANONICAL_PRIVATE_SYNCHRONOUS in self.hints:
139
return str(self.hints[Message.X_CANONICAL_PRIVATE_SYNCHRONOUS]) == "volume"
141
def log_message(self):
142
"""Write debug info about a message."""
145
"Message created at: " + Timestamp.locale_datetime(self.timestamp),
146
"app_name: " + repr(self.app_name),
147
"replaces_id: " + repr(self.replaces_id),
148
"app_icon: " + repr(self.app_icon),
149
"summary: " + repr(self.summary),
150
"body: " + repr(self.body),
151
"actions: " + repr(self.actions),
152
"expire_timeout: " + repr(self.expire_timeout),
156
# Log all the hints except image_data
157
for key in self.hints:
158
if key != "image_data":
159
result.append(" " + str(key) + ": " + repr(self.hints[key]))
161
# Log info about the image_data
162
if self.image_data != None:
163
result.append("image_data:")
164
result.append(" width: " + repr(self.image_data.width))
165
result.append(" height: " + repr(self.image_data.height))
166
result.append(" rowstride: " + repr(self.image_data.rowstride))
167
result.append(" has_alpha: " + repr(self.image_data.has_alpha))
168
result.append(" bits_per_sample: " + repr(self.image_data.bits_per_sample))
169
result.append(" channels: " + repr(self.image_data.channels))
171
result.append("-" * 50)
173
logger.debug("\n" + "\n".join(result))
175
class Notification(GObject.GObject):
176
"""Monitors DBUS for org.freedesktop.Notifications.Notify messages, parses them,
177
and notifies listeners when they arrive."""
179
"message-received": (GObject.SignalFlags.RUN_LAST, None, [Message])
182
GObject.GObject.__init__(self)
184
self._blacklist = None
185
self._match_string = "type='method_call',interface='org.freedesktop.Notifications',member='Notify'"
187
DBusGMainLoop(set_as_default=True)
188
self._bus = dbus.SessionBus()
189
self._bus.add_match_string(self._match_string)
190
self._bus.add_message_filter(self._message_filter)
193
"""Closes the connection to the session bus."""
196
def set_blacklist(self, blacklist):
197
"""Defines the set of app_names of messages to be discarded."""
198
self._blacklist = blacklist
200
def _message_filter(self, connection, dbus_message):
201
"""Triggers when messages are received from the session bus."""
202
if dbus_message.get_member() == "Notify" and dbus_message.get_interface() == "org.freedesktop.Notifications":
204
message = Message(dbus_message, Timestamp.now())
206
logger.exception("Failed to parse dbus message: " + repr(dbus_message.get_args_list()))
208
# Discard unwanted messages
209
if message.is_volume_notification():
211
if self._blacklist and self._blacklist.get_bool(message.app_name, False):
213
logger.debug("Sending message-received")
214
self.emit("message-received", message)
216
GObject.type_register(Message)
217
GObject.type_register(Notification)