| 1 | # |
| 2 | # Copyright (C) 2008 OLPC |
| 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 | # |
| 9 | # This program is distributed in the hope that it will be useful, |
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | # GNU General Public License for more details. |
| 13 | # |
| 14 | # You should have received a copy of the GNU General Public License |
| 15 | # along with this program; if not, write to the Free Software |
| 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| 17 | |
| 18 | from gettext import gettext as _ |
| 19 | import logging |
| 20 | import sha |
| 21 | |
| 22 | import gtk |
| 23 | import dbus |
| 24 | |
| 25 | from sugar.graphics.icon import get_icon_state |
| 26 | from sugar.graphics import style |
| 27 | from sugar.graphics.palette import Palette |
| 28 | from sugar.graphics.toolbutton import ToolButton |
| 29 | from sugar.graphics import xocolor |
| 30 | |
| 31 | from jarabe.model import network |
| 32 | from jarabe.frame.frameinvoker import FrameWidgetInvoker |
| 33 | from jarabe.view.pulsingicon import PulsingIcon |
| 34 | |
| 35 | _ICON_NAME = 'network-wireless' |
| 36 | |
| 37 | IP_ADDRESS_TEXT_TEMPLATE = _("IP address: %s") |
| 38 | |
| 39 | _NM_SERVICE = 'org.freedesktop.NetworkManager' |
| 40 | _NM_IFACE = 'org.freedesktop.NetworkManager' |
| 41 | _NM_PATH = '/org/freedesktop/NetworkManager' |
| 42 | _NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' |
| 43 | _NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' |
| 44 | _NM_ACCESSPOINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' |
| 45 | _NM_ACTIVE_CONN_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' |
| 46 | |
| 47 | _NM_DEVICE_STATE_UNKNOWN = 0 |
| 48 | _NM_DEVICE_STATE_UNMANAGED = 1 |
| 49 | _NM_DEVICE_STATE_UNAVAILABLE = 2 |
| 50 | _NM_DEVICE_STATE_DISCONNECTED = 3 |
| 51 | _NM_DEVICE_STATE_PREPARE = 4 |
| 52 | _NM_DEVICE_STATE_CONFIG = 5 |
| 53 | _NM_DEVICE_STATE_NEED_AUTH = 6 |
| 54 | _NM_DEVICE_STATE_IP_CONFIG = 7 |
| 55 | _NM_DEVICE_STATE_ACTIVATED = 8 |
| 56 | _NM_DEVICE_STATE_FAILED = 9 |
| 57 | |
| 58 | def freq_to_channel(freq): |
| 59 | ftoc = { 2412: 1, 2417: 2, 2422: 3, 2427: 4, |
| 60 | 2432: 5, 2437: 6, 2442: 7, 2447: 8, |
| 61 | 2452: 9, 2457: 10, 2462: 11, 2467: 12, |
| 62 | 2472: 13} |
| 63 | return ftoc[freq] |
| 64 | |
| 65 | class WirelessPalette(Palette): |
| 66 | def __init__(self, primary_text, device_view): |
| 67 | Palette.__init__(self, label=primary_text) |
| 68 | |
| 69 | self._device_view = device_view |
| 70 | self._disconnect_item = None |
| 71 | |
| 72 | self._chan_label = gtk.Label() |
| 73 | self._chan_label.props.xalign = 0.0 |
| 74 | self._chan_label.show() |
| 75 | |
| 76 | self._ip_address_label = gtk.Label() |
| 77 | |
| 78 | self._info = gtk.VBox() |
| 79 | |
| 80 | def _padded(child, xalign=0, yalign=0.5): |
| 81 | padder = gtk.Alignment(xalign=xalign, yalign=yalign, |
| 82 | xscale=1, yscale=0.33) |
| 83 | padder.set_padding(style.DEFAULT_SPACING, |
| 84 | style.DEFAULT_SPACING, |
| 85 | style.DEFAULT_SPACING, |
| 86 | style.DEFAULT_SPACING) |
| 87 | padder.add(child) |
| 88 | return padder |
| 89 | |
| 90 | self._info.pack_start(_padded(self._chan_label)) |
| 91 | self._info.pack_start(_padded(self._ip_address_label)) |
| 92 | self._info.show_all() |
| 93 | |
| 94 | self._disconnect_item = gtk.MenuItem(_('Disconnect...')) |
| 95 | self._disconnect_item.connect('activate', self._disconnect_activate_cb) |
| 96 | self.menu.append(self._disconnect_item) |
| 97 | |
| 98 | self.disconnected() |
| 99 | |
| 100 | def connecting(self): |
| 101 | self.props.secondary_text = _('Connecting...') |
| 102 | |
| 103 | def connected(self, frequency, iaddress): |
| 104 | self.set_content(self._info) |
| 105 | self.props.secondary_text = _('Connected') |
| 106 | self._set_channel(frequency) |
| 107 | self._set_ip_address(iaddress) |
| 108 | self._disconnect_item.show() |
| 109 | |
| 110 | def disconnected(self): |
| 111 | self.props.primary_text = '' |
| 112 | self.props.secondary_text = _('Not connected') |
| 113 | self.set_content(None) |
| 114 | self._disconnect_item.hide() |
| 115 | |
| 116 | def _disconnect_activate_cb(self, menuitem): |
| 117 | self._device_view.deactivate_connection() |
| 118 | |
| 119 | def _inet_ntoa(self, iaddress): |
| 120 | address = ['%s' % ((iaddress >> i) % 256) for i in [0, 8, 16, 24]] |
| 121 | return ".".join(address) |
| 122 | |
| 123 | def _set_channel(self, freq): |
| 124 | try: |
| 125 | chan = freq_to_channel(freq) |
| 126 | except KeyError: |
| 127 | chan = 0 |
| 128 | self._chan_label.set_text("%s: %d" % (_("Channel"), chan)) |
| 129 | |
| 130 | def _set_ip_address(self, ip_address): |
| 131 | if ip_address is not None: |
| 132 | ip_address_text = IP_ADDRESS_TEXT_TEMPLATE % \ |
| 133 | self._inet_ntoa(ip_address) |
| 134 | else: |
| 135 | ip_address_text = "" |
| 136 | self._ip_address_label.set_text(ip_address_text) |
| 137 | |
| 138 | |
| 139 | class WirelessDeviceView(ToolButton): |
| 140 | |
| 141 | FRAME_POSITION_RELATIVE = 300 |
| 142 | |
| 143 | def __init__(self, device): |
| 144 | ToolButton.__init__(self) |
| 145 | |
| 146 | self._bus = dbus.SystemBus() |
| 147 | self._device = device |
| 148 | self._flags = 0 |
| 149 | self._name = '' |
| 150 | self._strength = 0 |
| 151 | self._frequency = 0 |
| 152 | self._device_state = None |
| 153 | self._color = None |
| 154 | self._active_ap_op = None |
| 155 | |
| 156 | self._icon = PulsingIcon() |
| 157 | self._icon.props.icon_name = get_icon_state(_ICON_NAME, 0) |
| 158 | self._inactive_color = xocolor.XoColor( \ |
| 159 | "%s,%s" % (style.COLOR_BUTTON_GREY.get_svg(), |
| 160 | style.COLOR_TRANSPARENT.get_svg())) |
| 161 | self._icon.props.pulse_color = self._inactive_color |
| 162 | self._icon.props.base_color = self._inactive_color |
| 163 | |
| 164 | self.set_icon_widget(self._icon) |
| 165 | self._icon.show() |
| 166 | |
| 167 | self._palette = WirelessPalette(self._name, self) |
| 168 | self.set_palette(self._palette) |
| 169 | self._palette.props.invoker = FrameWidgetInvoker(self) |
| 170 | self._palette.set_group_id('frame') |
| 171 | |
| 172 | self._device.Get(_NM_WIRELESS_IFACE, 'ActiveAccessPoint', |
| 173 | reply_handler=self.__get_active_ap_reply_cb, |
| 174 | error_handler=self.__get_active_ap_error_cb) |
| 175 | |
| 176 | self._bus.add_signal_receiver(self.__state_changed_cb, |
| 177 | signal_name='StateChanged', |
| 178 | path=self._device.object_path, |
| 179 | dbus_interface=_NM_DEVICE_IFACE) |
| 180 | |
| 181 | def disconnect(self): |
| 182 | self._bus.remove_signal_receiver(self.__state_changed_cb, |
| 183 | signal_name='StateChanged', |
| 184 | path=self._device.object_path, |
| 185 | dbus_interface=_NM_DEVICE_IFACE) |
| 186 | |
| 187 | def __get_active_ap_reply_cb(self, active_ap_op): |
| 188 | if self._active_ap_op != active_ap_op: |
| 189 | if self._active_ap_op is not None: |
| 190 | self._bus.remove_signal_receiver( |
| 191 | self.__ap_properties_changed_cb, |
| 192 | signal_name='PropertiesChanged', |
| 193 | path=self._active_ap_op, |
| 194 | dbus_interface=_NM_ACCESSPOINT_IFACE) |
| 195 | if active_ap_op == '/': |
| 196 | self._active_ap_op = None |
| 197 | return |
| 198 | self._active_ap_op = active_ap_op |
| 199 | active_ap = self._bus.get_object(_NM_SERVICE, active_ap_op) |
| 200 | props = dbus.Interface(active_ap, 'org.freedesktop.DBus.Properties') |
| 201 | |
| 202 | props.GetAll(_NM_ACCESSPOINT_IFACE, byte_arrays=True, |
| 203 | reply_handler=self.__get_all_props_reply_cb, |
| 204 | error_handler=self.__get_all_props_error_cb) |
| 205 | |
| 206 | self._bus.add_signal_receiver(self.__ap_properties_changed_cb, |
| 207 | signal_name='PropertiesChanged', |
| 208 | path=self._active_ap_op, |
| 209 | dbus_interface=_NM_ACCESSPOINT_IFACE) |
| 210 | |
| 211 | def __get_active_ap_error_cb(self, err): |
| 212 | logging.debug('Error getting the active access point: %s', err) |
| 213 | |
| 214 | def __state_changed_cb(self, new_state, old_state, reason): |
| 215 | self._device_state = new_state |
| 216 | self._update_state() |
| 217 | |
| 218 | self._device.Get(_NM_WIRELESS_IFACE, 'ActiveAccessPoint', |
| 219 | reply_handler=self.__get_active_ap_reply_cb, |
| 220 | error_handler=self.__get_active_ap_error_cb) |
| 221 | |
| 222 | def __ap_properties_changed_cb(self, properties): |
| 223 | self._update_properties(properties) |
| 224 | |
| 225 | def _update_properties(self, properties): |
| 226 | if 'Ssid' in properties: |
| 227 | self._name = properties['Ssid'] |
| 228 | if 'Strength' in properties: |
| 229 | self._strength = properties['Strength'] |
| 230 | if 'Flags' in properties: |
| 231 | self._flags = properties['Flags'] |
| 232 | if 'Frequency' in properties: |
| 233 | self._frequency = properties['Frequency'] |
| 234 | |
| 235 | sh = sha.new() |
| 236 | data = self._name + hex(self._flags) |
| 237 | sh.update(data) |
| 238 | h = hash(sh.digest()) |
| 239 | idx = h % len(xocolor.colors) |
| 240 | |
| 241 | self._color = xocolor.XoColor('%s,%s' % (xocolor.colors[idx][0], |
| 242 | xocolor.colors[idx][1])) |
| 243 | self._update() |
| 244 | |
| 245 | def __get_all_props_reply_cb(self, properties): |
| 246 | self._update_properties(properties) |
| 247 | |
| 248 | def __get_all_props_error_cb(self, err): |
| 249 | logging.debug('Error getting the access point properties: %s', err) |
| 250 | |
| 251 | def _update(self): |
| 252 | if self._flags == network.NM_802_11_AP_FLAGS_PRIVACY: |
| 253 | self._icon.props.badge_name = "emblem-locked" |
| 254 | else: |
| 255 | self._icon.props.badge_name = None |
| 256 | |
| 257 | self._palette.props.primary_text = self._name |
| 258 | |
| 259 | self._update_state() |
| 260 | self._update_color() |
| 261 | |
| 262 | def _update_state(self): |
| 263 | if self._active_ap_op is not None: |
| 264 | state = self._device_state |
| 265 | else: |
| 266 | state = network.DEVICE_STATE_UNKNOWN |
| 267 | |
| 268 | if state == network.DEVICE_STATE_ACTIVATED: |
| 269 | icon_name = '%s-connected' % _ICON_NAME |
| 270 | else: |
| 271 | icon_name = _ICON_NAME |
| 272 | |
| 273 | icon_name = get_icon_state(icon_name, self._strength) |
| 274 | if icon_name: |
| 275 | self._icon.props.icon_name = icon_name |
| 276 | |
| 277 | if state == network.DEVICE_STATE_PREPARE or \ |
| 278 | state == network.DEVICE_STATE_CONFIG or \ |
| 279 | state == network.DEVICE_STATE_NEED_AUTH or \ |
| 280 | state == network.DEVICE_STATE_IP_CONFIG: |
| 281 | self._palette.connecting() |
| 282 | self._icon.props.pulsing = True |
| 283 | elif state == network.DEVICE_STATE_ACTIVATED: |
| 284 | props = dbus.Interface(self._device, |
| 285 | 'org.freedesktop.DBus.Properties') |
| 286 | address = props.Get(_NM_DEVICE_IFACE, 'Ip4Address') |
| 287 | self._palette.connected(self._frequency, address) |
| 288 | self._icon.props.pulsing = False |
| 289 | else: |
| 290 | self._palette.disconnected() |
| 291 | self._icon.props.pulsing = False |
| 292 | self._icon.props.base_color = self._inactive_color |
| 293 | self._icon.props.badge_name = None |
| 294 | self._name = '' |
| 295 | |
| 296 | def _update_color(self): |
| 297 | self._icon.props.base_color = self._color |
| 298 | |
| 299 | def deactivate_connection(self): |
| 300 | if self._active_ap_op is not None: |
| 301 | obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) |
| 302 | netmgr = dbus.Interface(obj, _NM_IFACE) |
| 303 | netmgr_props = dbus.Interface( |
| 304 | netmgr, 'org.freedesktop.DBus.Properties') |
| 305 | active_connections_o = netmgr_props.Get(_NM_IFACE, |
| 306 | 'ActiveConnections') |
| 307 | |
| 308 | for conn_o in active_connections_o: |
| 309 | obj = self._bus.get_object(_NM_IFACE, conn_o) |
| 310 | props = dbus.Interface(obj, 'org.freedesktop.DBus.Properties') |
| 311 | ap_op = props.Get(_NM_ACTIVE_CONN_IFACE, 'SpecificObject') |
| 312 | if ap_op == self._active_ap_op: |
| 313 | netmgr.DeactivateConnection(conn_o) |
| 314 | break |
| 315 | |
| 316 | class NetworkManagerObserver(object): |
| 317 | def __init__(self, tray): |
| 318 | self._bus = dbus.SystemBus() |
| 319 | self._devices = {} |
| 320 | self._netmgr = None |
| 321 | self._tray = tray |
| 322 | |
| 323 | try: |
| 324 | obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) |
| 325 | self._netmgr = dbus.Interface(obj, _NM_IFACE) |
| 326 | except dbus.DBusException: |
| 327 | logging.debug('%s service not available', _NM_SERVICE) |
| 328 | return |
| 329 | |
| 330 | self._netmgr.GetDevices(reply_handler=self.__get_devices_reply_cb, |
| 331 | error_handler=self.__get_devices_error_cb) |
| 332 | |
| 333 | self._bus.add_signal_receiver(self.__device_added_cb, |
| 334 | signal_name='DeviceAdded', |
| 335 | dbus_interface=_NM_IFACE) |
| 336 | self._bus.add_signal_receiver(self.__device_removed_cb, |
| 337 | signal_name='DeviceRemoved', |
| 338 | dbus_interface=_NM_IFACE) |
| 339 | |
| 340 | def __get_devices_reply_cb(self, devices_o): |
| 341 | for dev_o in devices_o: |
| 342 | self._check_device(dev_o) |
| 343 | |
| 344 | def __get_devices_error_cb(self, err): |
| 345 | logging.error('Failed to get devices: %s', err) |
| 346 | |
| 347 | def _check_device(self, device_o): |
| 348 | nm_device = self._bus.get_object(_NM_SERVICE, device_o) |
| 349 | props = dbus.Interface(nm_device, 'org.freedesktop.DBus.Properties') |
| 350 | |
| 351 | device_type = props.Get(_NM_DEVICE_IFACE, 'DeviceType') |
| 352 | if device_type == network.DEVICE_TYPE_802_11_WIRELESS: |
| 353 | device = WirelessDeviceView(nm_device) |
| 354 | self._devices[device_o] = device |
| 355 | self._tray.add_device(device) |
| 356 | |
| 357 | def __device_added_cb(self, device_o): |
| 358 | self._check_device(device_o) |
| 359 | |
| 360 | def __device_removed_cb(self, device_o): |
| 361 | if device_o in self._devices: |
| 362 | device = self._devices[device_o] |
| 363 | device.disconnect() |
| 364 | self._tray.remove_device(device) |
| 365 | del self._devices[device_o] |
| 366 | |
| 367 | def setup(tray): |
| 368 | device_observer = NetworkManagerObserver(tray) |