diff options
Diffstat (limited to 'src/jarabe')
23 files changed, 1960 insertions, 389 deletions
diff --git a/src/jarabe/desktop/activitieslist.py b/src/jarabe/desktop/activitieslist.py index 5d6f900..90b0752 100644 --- a/src/jarabe/desktop/activitieslist.py +++ b/src/jarabe/desktop/activitieslist.py @@ -14,7 +14,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import os import logging +from gettext import gettext as _ import gobject import gtk @@ -23,8 +25,10 @@ import gconf from sugar import util from sugar.graphics import style -from sugar.graphics.icon import CanvasIcon +from sugar.graphics.icon import CanvasIcon, Icon from sugar.graphics.xocolor import XoColor +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.alert import Alert from sugar.activity import activityfactory from sugar.activity.activityhandle import ActivityHandle @@ -35,11 +39,6 @@ from jarabe.view import launcher class ActivitiesList(gtk.VBox): __gtype_name__ = 'SugarActivitiesList' - __gsignals__ = { - 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([str])) - } - def __init__(self): logging.debug('STARTUP: Loading the activities list') @@ -94,7 +93,33 @@ class ActivitiesList(gtk.VBox): entry.set_visible(entry.matches(self._query)) def __erase_activated_cb(self, activity_icon, bundle_id): - self.emit('erase-activated', bundle_id) + registry = bundleregistry.get_registry() + activity_info = registry.get_bundle(bundle_id) + + alert = Alert() + alert.props.title = _('Confirm erase') + alert.props.msg = \ + _('Confirm erase: Do you want to permanently erase %s?') \ + % activity_info.get_name() + + cancel_icon = Icon(icon_name='dialog-cancel') + alert.add_button(gtk.RESPONSE_CANCEL, _('Keep'), cancel_icon) + + erase_icon = Icon(icon_name='dialog-ok') + alert.add_button(gtk.RESPONSE_OK, _('Erase'), erase_icon) + + alert.connect('response', self.__erase_confirmation_dialog_response_cb, + bundle_id) + + self.add_alert(alert) + + def __erase_confirmation_dialog_response_cb(self, alert, response_id, + bundle_id): + self.remove_alert() + if response_id == gtk.RESPONSE_OK: + registry = bundleregistry.get_registry() + bundle = registry.get_bundle(bundle_id) + registry.uninstall(bundle) def set_filter(self, query): self._query = query @@ -150,11 +175,11 @@ class ActivityIcon(CanvasIcon): self._xocolor = XoColor(client.get_string("/desktop/sugar/user/color")) def create_palette(self): - palette = ActivityPalette(self._activity_info) + palette = ActivityListPalette(self._activity_info) palette.connect('erase-activated', self.__erase_activated_cb) return palette - def __erase_activated_cb(self, palette): + def __erase_activated_cb(self, palette, bundle_id): self.emit('erase-activated', self._activity_info.get_bundle_id()) def _color(self): @@ -178,7 +203,7 @@ class ActivityEntry(hippo.CanvasBox, hippo.CanvasItem): __gtype_name__ = 'SugarActivityEntry' _TITLE_COL_WIDTH = style.GRID_CELL_SIZE * 3 - _VERSION_COL_WIDTH = style.GRID_CELL_SIZE * 1 + _VERSION_COL_WIDTH = style.GRID_CELL_SIZE * 3 _DATE_COL_WIDTH = style.GRID_CELL_SIZE * 5 def __init__(self, activity_info): @@ -322,3 +347,84 @@ class FavoriteIcon(CanvasIcon): icon.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() elif event.detail == hippo.MOTION_DETAIL_LEAVE: icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + +class ActivityListPalette(ActivityPalette): + __gtype_name__ = 'SugarActivityListPalette' + + __gsignals__ = { + 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([str])) + } + + def __init__(self, activity_info): + ActivityPalette.__init__(self, activity_info) + + self._bundle_id = activity_info.get_bundle_id() + self._version = activity_info.get_activity_version() + + registry = bundleregistry.get_registry() + self._favorite = registry.is_bundle_favorite(self._bundle_id, + self._version) + + self._favorite_item = MenuItem('') + self._favorite_icon = Icon(icon_name='emblem-favorite', + icon_size=gtk.ICON_SIZE_MENU) + self._favorite_item.set_image(self._favorite_icon) + self._favorite_item.connect('activate', + self.__change_favorite_activate_cb) + self.menu.append(self._favorite_item) + self._favorite_item.show() + + self._add_erase_option(registry, activity_info) + + registry = bundleregistry.get_registry() + self._activity_changed_sid = registry.connect('bundle_changed', + self.__activity_changed_cb) + self._update_favorite_item() + + self.connect('destroy', self.__destroy_cb) + + def _add_erase_option(self, registry, activity_info): + menu_item = MenuItem(_('Erase'), 'list-remove') + menu_item.connect('activate', self.__erase_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + if not os.access(activity_info.get_path(), os.W_OK) or \ + registry.is_activity_protected(self._bundle_id): + menu_item.props.sensitive = False + + + def __destroy_cb(self, palette): + self.disconnect(self._activity_changed_sid) + + def _update_favorite_item(self): + label = self._favorite_item.child + if self._favorite: + label.set_text(_('Remove favorite')) + xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + else: + label.set_text(_('Make favorite')) + client = gconf.client_get_default() + xo_color = XoColor(client.get_string("/desktop/sugar/user/color")) + + self._favorite_icon.props.xo_color = xo_color + + def __change_favorite_activate_cb(self, menu_item): + registry = bundleregistry.get_registry() + registry.set_bundle_favorite(self._bundle_id, + self._version, + not self._favorite) + + def __activity_changed_cb(self, activity_registry, activity_info): + if activity_info.get_bundle_id() == self._bundle_id and \ + activity_info.get_activity_version() == self._version: + registry = bundleregistry.get_registry() + self._favorite = registry.is_bundle_favorite(self._bundle_id, + self._version) + self._update_favorite_item() + + def __erase_activate_cb(self, menu_item): + self.emit('erase-activated', self._bundle_id) + diff --git a/src/jarabe/desktop/favoritesview.py b/src/jarabe/desktop/favoritesview.py index 5ea76b8..848ee9e 100644 --- a/src/jarabe/desktop/favoritesview.py +++ b/src/jarabe/desktop/favoritesview.py @@ -65,11 +65,6 @@ about the layout can be accessed with fields of the class.""" class FavoritesView(hippo.Canvas): __gtype_name__ = 'SugarFavoritesView' - __gsignals__ = { - 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([str])) - } - def __init__(self, **kwargs): logging.debug('STARTUP: Loading the favorites view') @@ -134,14 +129,10 @@ class FavoritesView(hippo.Canvas): if activity_info.get_bundle_id() == 'org.laptop.JournalActivity': return icon = ActivityIcon(activity_info, self._datastore_listener) - icon.connect('erase-activated', self.__erase_activated_cb) icon.props.size = style.STANDARD_ICON_SIZE self._box.insert_sorted(icon, 0, self._layout.compare_activities) self._layout.append(icon) - def __erase_activated_cb(self, activity_icon, bundle_id): - self.emit('erase-activated', bundle_id) - def __activity_added_cb(self, activity_registry, activity_info): registry = bundleregistry.get_registry() if registry.is_bundle_favorite(activity_info.get_bundle_id(), @@ -282,7 +273,9 @@ class FavoritesView(hippo.Canvas): def _set_layout(self, layout): if layout not in LAYOUT_MAP: - raise ValueError('Unknown favorites layout: %r' % layout) + logging.warn('Unknown favorites layout: %r' % layout) + layout = favoriteslayout.RingLayout.key + assert layout in LAYOUT_MAP if type(self._layout) == LAYOUT_MAP[layout]: return @@ -402,11 +395,6 @@ class ActivityIcon(CanvasIcon): _BORDER_WIDTH = style.zoom(3) - __gsignals__ = { - 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([str])) - } - def __init__(self, activity_info, datastore_listener): CanvasIcon.__init__(self, cache=True, file_name=activity_info.get_icon()) @@ -469,12 +457,8 @@ class ActivityIcon(CanvasIcon): def create_palette(self): palette = FavoritePalette(self._activity_info, self._journal_entries) palette.connect('activate', self.__palette_activate_cb) - palette.connect('erase-activated', self.__erase_activated_cb) return palette - def __erase_activated_cb(self, palette): - self.emit('erase-activated', self._activity_info.get_bundle_id()) - def __palette_activate_cb(self, palette): self._activate() @@ -609,7 +593,7 @@ class FavoritePalette(ActivityPalette): def __resume_entry_cb(self, menu_item, entry): if entry is not None: - journal.misc.resume(entry, self._bundle_id) + journal.misc.resume(entry, entry['activity']) class CurrentActivityIcon(CanvasIcon, hippo.CanvasItem): def __init__(self): @@ -688,7 +672,7 @@ class _MyIcon(MyIcon): self.emit('register-activate') def remove_register_menu(self): - self.palette.remove(self._register_menu) + self.palette.menu.remove(self._register_menu) class FavoritesSetting(object): diff --git a/src/jarabe/desktop/homebox.py b/src/jarabe/desktop/homebox.py index 6fdc8f1..fdfb7a4 100644 --- a/src/jarabe/desktop/homebox.py +++ b/src/jarabe/desktop/homebox.py @@ -27,7 +27,6 @@ from sugar.graphics.radiotoolbutton import RadioToolButton from sugar.graphics.alert import Alert from sugar.graphics.icon import Icon -from jarabe.model import bundleregistry from jarabe.desktop import favoritesview from jarabe.desktop.activitieslist import ActivitiesList @@ -47,10 +46,6 @@ class HomeBox(gtk.VBox): self._favorites_view = favoritesview.FavoritesView() self._list_view = ActivitiesList() - self._favorites_view.connect('erase-activated', - self.__erase_activated_cb) - self._list_view.connect('erase-activated', self.__erase_activated_cb) - self._toolbar = HomeToolbar() self._toolbar.connect('query-changed', self.__toolbar_query_changed_cb) self._toolbar.connect('view-changed', self.__toolbar_view_changed_cb) @@ -59,44 +54,6 @@ class HomeBox(gtk.VBox): self._set_view(_FAVORITES_VIEW) - def __erase_activated_cb(self, view, bundle_id): - registry = bundleregistry.get_registry() - activity_info = registry.get_bundle(bundle_id) - - alert = Alert() - alert.props.title = _('Confirm erase') - alert.props.msg = \ - _('Confirm erase: Do you want to permanently erase %s?') \ - % activity_info.get_name() - - cancel_icon = Icon(icon_name='dialog-cancel') - alert.add_button(gtk.RESPONSE_CANCEL, _('Keep'), cancel_icon) - - erase_icon = Icon(icon_name='dialog-ok') - alert.add_button(gtk.RESPONSE_OK, _('Erase'), erase_icon) - - if self._list_view in self.get_children(): - self._list_view.add_alert(alert) - else: - self._favorites_view.add_alert(alert) - # TODO: If the favorite layouts didn't hardcoded the box size, we could - # just pack an alert between the toolbar and the canvas. - #self.pack_start(alert, False) - #self.reorder_child(alert, 1) - alert.connect('response', self.__erase_confirmation_dialog_response_cb, - bundle_id) - - def __erase_confirmation_dialog_response_cb(self, alert, response_id, - bundle_id): - if self._list_view in self.get_children(): - self._list_view.remove_alert() - else: - self._favorites_view.remove_alert() - if response_id == gtk.RESPONSE_OK: - registry = bundleregistry.get_registry() - bundle = registry.get_bundle(bundle_id) - registry.uninstall(bundle) - def show_software_updates_alert(self): alert = Alert() updater_icon = Icon(icon_name='module-updater', diff --git a/src/jarabe/desktop/homewindow.py b/src/jarabe/desktop/homewindow.py index bbb0db1..19cc5a2 100644 --- a/src/jarabe/desktop/homewindow.py +++ b/src/jarabe/desktop/homewindow.py @@ -82,9 +82,9 @@ class HomeWindow(gtk.Window): def _visibility_notify_event_cb(self, window, event): if event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED: - self._deactivate_view() + self._deactivate_view(shell.get_model().zoom_level) else: - self._activate_view() + self._activate_view(shell.get_model().zoom_level) def __zoom_level_changed_cb(self, **kwargs): old_level = kwargs['old_level'] diff --git a/src/jarabe/desktop/meshbox.py b/src/jarabe/desktop/meshbox.py index e7bba7b..41ffcc6 100644 --- a/src/jarabe/desktop/meshbox.py +++ b/src/jarabe/desktop/meshbox.py @@ -1,6 +1,6 @@ # Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer -# Copyright (C) 2009 One Laptop per Child +# Copyright (C) 2009-2010 One Laptop per Child # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,6 +24,7 @@ import dbus import hippo import gobject import gtk +import gconf from sugar.graphics.icon import CanvasIcon, Icon from sugar.graphics.xocolor import XoColor @@ -36,6 +37,7 @@ from sugar.graphics.menuitem import MenuItem from sugar.activity.activityhandle import ActivityHandle from sugar.activity import activityfactory from sugar.util import unique_id +from sugar import profile from jarabe.model import neighborhood from jarabe.view.buddyicon import BuddyIcon @@ -51,17 +53,21 @@ from jarabe.model.network import Settings from jarabe.model.network import IP4Config from jarabe.model.network import WirelessSecurity from jarabe.model.network import AccessPoint +from jarabe.model.network import OlpcMesh as OlpcMeshSettings +from jarabe.model.olpcmesh import OlpcMeshManager +from jarabe.model.adhoc import get_adhoc_manager_instance _NM_SERVICE = 'org.freedesktop.NetworkManager' _NM_IFACE = 'org.freedesktop.NetworkManager' _NM_PATH = '/org/freedesktop/NetworkManager' _NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' _NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' +_NM_OLPC_MESH_IFACE = 'org.freedesktop.NetworkManager.Device.OlpcMesh' _NM_ACCESSPOINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' _NM_ACTIVE_CONN_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' -_ICON_NAME = 'network-wireless' - +_AP_ICON_NAME = 'network-wireless' +_OLPC_MESH_ICON_NAME = 'network-mesh' class WirelessNetworkView(CanvasPulsingIcon): def __init__(self, initial_ap): @@ -83,14 +89,11 @@ class WirelessNetworkView(CanvasPulsingIcon): self._rsn_flags = initial_ap.rsn_flags self._device_caps = 0 self._device_state = None - self._connection = None self._color = None - if self._mode == network.NM_802_11_MODE_ADHOC \ - and self._name_encodes_colors(): - encoded_color = self._name.split("#", 1) - if len(encoded_color) == 2: - self._color = xocolor.XoColor('#' + encoded_color[1]) + if self._mode == network.NM_802_11_MODE_ADHOC and \ + network.is_sugar_adhoc_network(self._name): + self._color = profile.get_color() else: sh = sha.new() data = self._name + hex(self._flags) @@ -112,18 +115,9 @@ class WirelessNetworkView(CanvasPulsingIcon): self.set_palette(self._palette) self._palette_icon.props.xo_color = self._color - if network.find_connection(self._name) is not None: - self.props.badge_name = "emblem-favorite" - self._palette_icon.props.badge_name = "emblem-favorite" - elif initial_ap.flags == network.NM_802_11_AP_FLAGS_PRIVACY: - self.props.badge_name = "emblem-locked" - self._palette_icon.props.badge_name = "emblem-locked" - else: - self.props.badge_name = None - self._palette_icon.props.badge_name = None + self._update_badge() - interface_props = dbus.Interface(self._device, - 'org.freedesktop.DBus.Properties') + interface_props = dbus.Interface(self._device, dbus.PROPERTIES_IFACE) interface_props.Get(_NM_DEVICE_IFACE, 'State', reply_handler=self.__get_device_state_reply_cb, error_handler=self.__get_device_state_error_cb) @@ -143,17 +137,12 @@ class WirelessNetworkView(CanvasPulsingIcon): path=self._device.object_path, dbus_interface=_NM_WIRELESS_IFACE) - def _name_encodes_colors(self): - """Match #XXXXXX,#YYYYYY at the end of the network name""" - return self._name[-7] == '#' and self._name[-8] == ',' \ - and self._name[-15] == '#' - def _create_palette(self): - icon_name = get_icon_state(_ICON_NAME, self._strength) + icon_name = get_icon_state(_AP_ICON_NAME, self._strength) self._palette_icon = Icon(icon_name=icon_name, icon_size=style.STANDARD_ICON_SIZE, badge_name=self.props.badge_name) - + p = palette.Palette(primary_text=self._name, icon=self._palette_icon) @@ -171,6 +160,8 @@ class WirelessNetworkView(CanvasPulsingIcon): def __device_state_changed_cb(self, new_state, old_state, reason): self._device_state = new_state self._update_state() + self._update_icon() + self._update_badge() def __update_active_ap(self, ap_path): if ap_path in self._access_points: @@ -178,12 +169,10 @@ class WirelessNetworkView(CanvasPulsingIcon): # strength of that one self._active_ap = self._access_points[ap_path] self.update_strength() - self._update_state() elif self._active_ap is not None: # revert to showing state of strongest AP again self._active_ap = None self.update_strength() - self._update_state() def __wireless_properties_changed_cb(self, properties): if 'ActiveAccessPoint' in properties: @@ -203,14 +192,38 @@ class WirelessNetworkView(CanvasPulsingIcon): def __get_device_state_reply_cb(self, state): self._device_state = state - self._update() + self._update_state() + self._update_color() + self._update_badge() def __get_device_state_error_cb(self, err): logging.error('Error getting the device state: %s', err) - def _update(self): - self._update_state() - self._update_color() + def _update_icon(self): + if self._mode == network.NM_802_11_MODE_ADHOC and \ + network.is_sugar_adhoc_network(self._name): + channel = max([1] + [ap.channel for ap in + self._access_points.values()]) + if self._device_state == network.DEVICE_STATE_ACTIVATED and \ + self._active_ap is not None: + icon_name = 'network-adhoc-%s-connected' % channel + else: + icon_name = 'network-adhoc-%s' % channel + self.props.icon_name = icon_name + icon = self._palette.props.icon + icon.props.icon_name = icon_name + else: + if self._device_state == network.DEVICE_STATE_ACTIVATED and \ + self._active_ap is not None: + icon_name = '%s-connected' % _AP_ICON_NAME + else: + icon_name = _AP_ICON_NAME + + icon_name = get_icon_state(icon_name, self._strength) + if icon_name: + self.props.icon_name = icon_name + icon = self._palette.props.icon + icon.props.icon_name = icon_name def _update_state(self): if self._active_ap is not None: @@ -218,22 +231,6 @@ class WirelessNetworkView(CanvasPulsingIcon): else: state = network.DEVICE_STATE_UNKNOWN - if state == network.DEVICE_STATE_ACTIVATED: - connection = network.find_connection(self._name) - if connection: - if self._mode == network.NM_802_11_MODE_INFRA: - connection.set_connected() - - icon_name = '%s-connected' % _ICON_NAME - else: - icon_name = _ICON_NAME - - icon_name = get_icon_state(icon_name, self._strength) - if icon_name: - self.props.icon_name = icon_name - icon = self._palette.props.icon - icon.props.icon_name = icon_name - if state == network.DEVICE_STATE_PREPARE or \ state == network.DEVICE_STATE_CONFIG or \ state == network.DEVICE_STATE_NEED_AUTH or \ @@ -244,6 +241,10 @@ class WirelessNetworkView(CanvasPulsingIcon): self._palette.props.secondary_text = _('Connecting...') self.props.pulsing = True elif state == network.DEVICE_STATE_ACTIVATED: + connection = network.find_connection_by_ssid(self._name) + if connection is not None: + if self._mode == network.NM_802_11_MODE_INFRA: + connection.set_connected() if self._disconnect_item: self._disconnect_item.show() self._connect_item.hide() @@ -256,15 +257,51 @@ class WirelessNetworkView(CanvasPulsingIcon): self._palette.props.secondary_text = None self.props.pulsing = False - def _update_color(self): + def _update_color(self): if self._greyed_out: self.props.pulsing = False self.props.base_color = XoColor('#D5D5D5,#D5D5D5') else: self.props.base_color = self._color + def _update_badge(self): + if self._mode != network.NM_802_11_MODE_ADHOC: + if network.find_connection_by_ssid(self._name) is not None: + self.props.badge_name = "emblem-favorite" + self._palette_icon.props.badge_name = "emblem-favorite" + elif self._flags == network.NM_802_11_AP_FLAGS_PRIVACY: + self.props.badge_name = "emblem-locked" + self._palette_icon.props.badge_name = "emblem-locked" + else: + self.props.badge_name = None + self._palette_icon.props.badge_name = None + else: + self.props.badge_name = None + self._palette_icon.props.badge_name = None + def _disconnect_activate_cb(self, item): - pass + connection = network.find_connection_by_ssid(self._name) + if connection: + if self._mode == network.NM_802_11_MODE_INFRA: + connection.set_disconnected() + + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + + netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE) + active_connections_o = netmgr_props.Get(_NM_IFACE, 'ActiveConnections') + + for conn_o in active_connections_o: + obj = self._bus.get_object(_NM_IFACE, conn_o) + props = dbus.Interface(obj, dbus.PROPERTIES_IFACE) + state = props.Get(_NM_ACTIVE_CONN_IFACE, 'State') + if state == network.NM_ACTIVE_CONNECTION_STATE_ACTIVATED: + ap_o = props.Get(_NM_ACTIVE_CONN_IFACE, 'SpecificObject') + if ap_o != '/' and self.find_ap(ap_o) is not None: + netmgr.DeactivateConnection(conn_o) + else: + logging.error('Could not determine AP for' + ' specific object %s' % conn_o) def _add_ciphers_from_flags(self, flags, pairwise): ciphers = [] @@ -336,11 +373,11 @@ class WirelessNetworkView(CanvasPulsingIcon): self._connect() def _connect(self): - connection = network.find_connection(self._name) + connection = network.find_connection_by_ssid(self._name) if connection is None: settings = Settings() settings.connection.id = 'Auto ' + self._name - settings.connection.uuid = unique_id() + uuid = settings.connection.uuid = unique_id() settings.connection.type = '802-11-wireless' settings.wireless.ssid = self._name @@ -358,7 +395,7 @@ class WirelessNetworkView(CanvasPulsingIcon): if wireless_security is not None: settings.wireless.security = '802-11-wireless-security' - connection = network.add_connection(self._name, settings) + connection = network.add_connection(uuid, settings) obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) netmgr = dbus.Interface(obj, _NM_IFACE) @@ -377,7 +414,7 @@ class WirelessNetworkView(CanvasPulsingIcon): def set_filter(self, query): self._greyed_out = self._name.lower().find(query) == -1 - self._update_state() + self._update_icon() self._update_color() def create_keydialog(self, settings, response): @@ -396,7 +433,7 @@ class WirelessNetworkView(CanvasPulsingIcon): if new_strength != self._strength: self._strength = new_strength - self._update_state() + self._update_icon() def add_ap(self, ap): self._access_points[ap.model.object_path] = ap @@ -419,6 +456,17 @@ class WirelessNetworkView(CanvasPulsingIcon): return None return self._access_points[ap_path] + def is_olpc_mesh(self): + return self._mode == network.NM_802_11_MODE_ADHOC \ + and self.name == "olpc-mesh" + + def remove_all_aps(self): + for ap in self._access_points.values(): + ap.disconnect() + self._access_points = {} + self._active_ap = None + self.update_strength() + def disconnect(self): self._bus.remove_signal_receiver(self.__device_state_changed_cb, signal_name='StateChanged', @@ -430,6 +478,280 @@ class WirelessNetworkView(CanvasPulsingIcon): dbus_interface=_NM_WIRELESS_IFACE) +class SugarAdhocView(CanvasPulsingIcon): + """To mimic the mesh behavior on devices where mesh hardware is + not available we support the creation of an Ad-hoc network on + three channels 1, 6, 11. This is the class for an icon + representing a channel in the neighborhood view. + + """ + + _ICON_NAME = 'network-adhoc-' + _NAME = 'Ad-hoc Network ' + + def __init__(self, channel): + CanvasPulsingIcon.__init__(self, + icon_name=self._ICON_NAME + str(channel), + size=style.STANDARD_ICON_SIZE, cache=True) + self._bus = dbus.SystemBus() + self._channel = channel + self._disconnect_item = None + self._connect_item = None + self._palette_icon = None + self._greyed_out = False + + get_adhoc_manager_instance().connect('members-changed', + self.__members_changed_cb) + get_adhoc_manager_instance().connect('state-changed', + self.__state_changed_cb) + + self.connect('button-release-event', self.__button_release_event_cb) + + pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self.props.pulse_color = pulse_color + self._state_color = XoColor('%s,%s' % \ + (profile.get_color().get_stroke_color(), + style.COLOR_TRANSPARENT.get_svg())) + self.props.base_color = self._state_color + self._palette = self._create_palette() + self.set_palette(self._palette) + self._palette_icon.props.xo_color = self._state_color + + def _create_palette(self): + self._palette_icon = Icon( \ + icon_name=self._ICON_NAME + str(self._channel), + icon_size=style.STANDARD_ICON_SIZE) + + palette_ = palette.Palette(_("Ad-hoc Network %d") % self._channel, + icon=self._palette_icon) + + self._connect_item = MenuItem(_('Connect'), 'dialog-ok') + self._connect_item.connect('activate', self.__connect_activate_cb) + palette_.menu.append(self._connect_item) + + self._disconnect_item = MenuItem(_('Disconnect'), 'media-eject') + self._disconnect_item.connect('activate', + self.__disconnect_activate_cb) + palette_.menu.append(self._disconnect_item) + + return palette_ + + def __button_release_event_cb(self, icon, event): + get_adhoc_manager_instance().activate_channel(self._channel) + + def __connect_activate_cb(self, icon): + get_adhoc_manager_instance().activate_channel(self._channel) + + def __disconnect_activate_cb(self, icon): + get_adhoc_manager_instance().deactivate_active_channel() + + def __state_changed_cb(self, adhoc_manager, channel, device_state): + if self._channel == channel: + state = device_state + else: + state = network.DEVICE_STATE_UNKNOWN + + if state == network.DEVICE_STATE_ACTIVATED: + icon_name = '%s-connected' % (self._ICON_NAME + str(self._channel)) + else: + icon_name = self._ICON_NAME + str(self._channel) + + if icon_name is not None: + self.props.icon_name = icon_name + icon = self._palette.props.icon + icon.props.icon_name = icon_name + + if state in [network.DEVICE_STATE_PREPARE, + network.DEVICE_STATE_CONFIG, + network.DEVICE_STATE_NEED_AUTH, + network.DEVICE_STATE_IP_CONFIG]: + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connecting...') + self.props.pulsing = True + elif state == network.DEVICE_STATE_ACTIVATED: + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connected') + self.props.pulsing = False + else: + if self._disconnect_item: + self._disconnect_item.hide() + self._connect_item.show() + self._palette.props.secondary_text = None + self.props.pulsing = False + + def _update_color(self): + if self._greyed_out: + self.props.pulsing = False + self.props.base_color = XoColor('#D5D5D5,#D5D5D5') + else: + self.props.base_color = self._state_color + + def __members_changed_cb(self, adhoc_manager, channel, has_members): + if channel == self._channel: + if has_members == True: + self._state_color = profile.get_color() + else: + color = '%s,%s' % (profile.get_color().get_stroke_color(), + style.COLOR_TRANSPARENT.get_svg()) + self._state_color = XoColor(color) + + if not self._greyed_out: + self.props.base_color = self._state_color + self._palette_icon.props.xo_color = self._state_color + + def set_filter(self, query): + name = self._NAME + str(self._channel) + self._greyed_out = name.lower().find(query) == -1 + self._update_color() + + +class OlpcMeshView(CanvasPulsingIcon): + def __init__(self, mesh_mgr, channel): + CanvasPulsingIcon.__init__(self, icon_name=_OLPC_MESH_ICON_NAME, + size=style.STANDARD_ICON_SIZE, cache=True) + self._bus = dbus.SystemBus() + self._channel = channel + self._mesh_mgr = mesh_mgr + self._disconnect_item = None + self._connect_item = None + self._greyed_out = False + self._name = '' + self._device_state = None + self._connection = None + self._active = False + device = mesh_mgr.mesh_device + + self.connect('button-release-event', self.__button_release_event_cb) + + interface_props = dbus.Interface(device, + 'org.freedesktop.DBus.Properties') + interface_props.Get(_NM_DEVICE_IFACE, 'State', + reply_handler=self.__get_device_state_reply_cb, + error_handler=self.__get_device_state_error_cb) + interface_props.Get(_NM_OLPC_MESH_IFACE, 'ActiveChannel', + reply_handler=self.__get_active_channel_reply_cb, + error_handler=self.__get_active_channel_error_cb) + + self._bus.add_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + self._bus.add_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=device.object_path, + dbus_interface=_NM_OLPC_MESH_IFACE) + + pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self.props.pulse_color = pulse_color + self.props.base_color = profile.get_color() + self._palette = self._create_palette() + self.set_palette(self._palette) + + def _create_palette(self): + _palette = palette.Palette(_("Mesh Network %d") % self._channel) + + self._connect_item = MenuItem(_('Connect'), 'dialog-ok') + self._connect_item.connect('activate', self.__connect_activate_cb) + _palette.menu.append(self._connect_item) + + return _palette + + def __get_device_state_reply_cb(self, state): + self._device_state = state + self._update() + + def __get_device_state_error_cb(self, err): + logging.error('Error getting the device state: %s', err) + + def __device_state_changed_cb(self, new_state, old_state, reason): + self._device_state = new_state + self._update() + + def __get_active_channel_reply_cb(self, channel): + self._active = (channel == self._channel) + self._update() + + def __get_active_channel_error_cb(self, err): + logging.error('Error getting the active channel: %s', err) + + def __wireless_properties_changed_cb(self, properties): + if 'ActiveChannel' in properties: + channel = properties['ActiveChannel'] + self._active = (channel == self._channel) + self._update() + + def _update(self): + if self._active: + state = self._device_state + else: + state = network.DEVICE_STATE_UNKNOWN + + if state in [network.DEVICE_STATE_PREPARE, + network.DEVICE_STATE_CONFIG, + network.DEVICE_STATE_NEED_AUTH, + network.DEVICE_STATE_IP_CONFIG]: + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connecting...') + self.props.pulsing = True + elif state == network.DEVICE_STATE_ACTIVATED: + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connected') + self.props.pulsing = False + else: + if self._disconnect_item: + self._disconnect_item.hide() + self._connect_item.show() + self._palette.props.secondary_text = None + self.props.pulsing = False + + def _update_color(self): + if self._greyed_out: + self.props.base_color = XoColor('#D5D5D5,#D5D5D5') + else: + self.props.base_color = profile.get_color() + + def __connect_activate_cb(self, icon): + self._connect() + + def __button_release_event_cb(self, icon, event): + self._connect() + + def _connect(self): + self._mesh_mgr.user_activate_channel(self._channel) + + def __activate_reply_cb(self, connection): + logging.debug('Connection activated: %s', connection) + + def __activate_error_cb(self, err): + logging.error('Failed to activate connection: %s', err) + + def set_filter(self, query): + self._greyed_out = (query != '') + self._update_color() + + def disconnect(self): + device_object_path = self._mesh_mgr.mesh_device.object_path + + self._bus.remove_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=device_object_path, + dbus_interface=_NM_DEVICE_IFACE) + self._bus.remove_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=device_object_path, + dbus_interface=_NM_OLPC_MESH_IFACE) + + class ActivityView(hippo.CanvasBox): def __init__(self, model): hippo.CanvasBox.__init__(self) @@ -616,13 +938,19 @@ class MeshToolbar(gtk.Toolbar): return False -class DeviceObserver(object): - def __init__(self, box, device): - self._box = box +class DeviceObserver(gobject.GObject): + __gsignals__ = { + 'access-point-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'access-point-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + def __init__(self, device): + gobject.GObject.__init__(self) self._bus = dbus.SystemBus() - self._device = device + self.device = device - wireless = dbus.Interface(self._device, _NM_WIRELESS_IFACE) + wireless = dbus.Interface(device, _NM_WIRELESS_IFACE) wireless.GetAccessPoints(reply_handler=self._get_access_points_reply_cb, error_handler=self._get_access_points_error_cb) @@ -638,35 +966,42 @@ class DeviceObserver(object): def _get_access_points_reply_cb(self, access_points_o): for ap_o in access_points_o: ap = self._bus.get_object(_NM_SERVICE, ap_o) - self._box.add_access_point(self._device, ap) + self.emit('access-point-added', ap) def _get_access_points_error_cb(self, err): logging.error('Failed to get access points: %s', err) def __access_point_added_cb(self, access_point_o): ap = self._bus.get_object(_NM_SERVICE, access_point_o) - self._box.add_access_point(self._device, ap) + self.emit('access-point-added', ap) def __access_point_removed_cb(self, access_point_o): - self._box.remove_access_point(access_point_o) + self.emit('access-point-removed', access_point_o) def disconnect(self): self._bus.remove_signal_receiver(self.__access_point_added_cb, signal_name='AccessPointAdded', - path=self._device.object_path, + path=self.device.object_path, dbus_interface=_NM_WIRELESS_IFACE) self._bus.remove_signal_receiver(self.__access_point_removed_cb, signal_name='AccessPointRemoved', - path=self._device.object_path, + path=self.device.object_path, dbus_interface=_NM_WIRELESS_IFACE) class NetworkManagerObserver(object): + + _SHOW_ADHOC_GCONF_KEY = '/desktop/sugar/network/adhoc' + def __init__(self, box): self._box = box self._bus = dbus.SystemBus() self._devices = {} self._netmgr = None + self._olpc_mesh_device_o = None + + client = gconf.client_get_default() + self._have_adhoc_networks = client.get_bool(self._SHOW_ADHOC_GCONF_KEY) def listen(self): try: @@ -685,6 +1020,9 @@ class NetworkManagerObserver(object): self._bus.add_signal_receiver(self.__device_removed_cb, signal_name='DeviceRemoved', dbus_interface=_NM_IFACE) + self._bus.add_signal_receiver(self.__properties_changed_cb, + signal_name='PropertiesChanged', + dbus_interface=_NM_IFACE) settings = network.get_settings() if settings is not None: @@ -694,13 +1032,12 @@ class NetworkManagerObserver(object): # FIXME It would be better to do all of this async, but I cannot think # of a good way to. NM could really use some love here. - netmgr_props = dbus.Interface( - self._netmgr, 'org.freedesktop.DBus.Properties') + netmgr_props = dbus.Interface(self._netmgr, dbus.PROPERTIES_IFACE) active_connections_o = netmgr_props.Get(_NM_IFACE, 'ActiveConnections') for conn_o in active_connections_o: obj = self._bus.get_object(_NM_IFACE, conn_o) - props = dbus.Interface(obj, 'org.freedesktop.DBus.Properties') + props = dbus.Interface(obj, dbus.PROPERTIES_IFACE) state = props.Get(_NM_ACTIVE_CONN_IFACE, 'State') if state == network.NM_ACTIVE_CONNECTION_STATE_ACTIVATING: ap_o = props.Get(_NM_ACTIVE_CONN_IFACE, 'SpecificObject') @@ -724,11 +1061,20 @@ class NetworkManagerObserver(object): def _check_device(self, device_o): device = self._bus.get_object(_NM_SERVICE, device_o) - props = dbus.Interface(device, 'org.freedesktop.DBus.Properties') + props = dbus.Interface(device, dbus.PROPERTIES_IFACE) device_type = props.Get(_NM_DEVICE_IFACE, 'DeviceType') if device_type == network.DEVICE_TYPE_802_11_WIRELESS: - self._devices[device_o] = DeviceObserver(self._box, device) + self._devices[device_o] = DeviceObserver(device) + self._devices[device_o].connect('access-point-added', + self.__ap_added_cb) + self._devices[device_o].connect('access-point-removed', + self.__ap_removed_cb) + if self._have_adhoc_networks: + self._box.add_adhoc_networks(device) + elif device_type == network.DEVICE_TYPE_802_11_OLPC_MESH: + self._olpc_mesh_device_o = device_o + self._box.enable_olpc_mesh(device) def _get_device_path_error_cb(self, err): logging.error('Failed to get device type: %s', err) @@ -741,6 +1087,28 @@ class NetworkManagerObserver(object): observer = self._devices[device_o] observer.disconnect() del self._devices[device_o] + if self._have_adhoc_networks: + self._box.remove_adhoc_networks() + return + + if self._olpc_mesh_device_o == device_o: + self._box.disable_olpc_mesh(device_o) + + def __ap_added_cb(self, device_observer, access_point): + self._box.add_access_point(device_observer.device, access_point) + + def __ap_removed_cb(self, device_observer, access_point_o): + self._box.remove_access_point(access_point_o) + + def __properties_changed_cb(self, properties): + if 'WirelessHardwareEnabled' in properties: + if properties['WirelessHardwareEnabled']: + if not self._have_adhoc_networks: + self._box.remove_adhoc_networks() + elif properties['WirelessHardwareEnabled']: + for device in self._devices: + if self._have_adhoc_networks: + self._box.add_adhoc_networks(device) class MeshBox(gtk.VBox): @@ -752,11 +1120,13 @@ class MeshBox(gtk.VBox): gobject.GObject.__init__(self) self.wireless_networks = {} + self._adhoc_manager = None + self._adhoc_networks = [] self._model = neighborhood.get_model() self._buddies = {} self._activities = {} - self._mesh = {} + self._mesh = [] self._buddy_to_activity = {} self._suspended = True self._query = '' @@ -901,6 +1271,23 @@ class MeshBox(gtk.VBox): del self.wireless_networks[hash] def _ap_props_changed_cb(self, ap, old_hash): + # if we have mesh hardware, ignore OLPC mesh networks that appear as + # normal wifi networks + if len(self._mesh) > 0 and ap.mode == network.NM_802_11_MODE_ADHOC \ + and ap.name == "olpc-mesh": + logging.debug("ignoring OLPC mesh IBSS") + ap.disconnect() + return + + if self._adhoc_manager is not None and \ + network.is_sugar_adhoc_network(ap.name) and \ + ap.mode == network.NM_802_11_MODE_ADHOC: + if old_hash is None: # new Ad-hoc network finished initializing + self._adhoc_manager.add_access_point(ap) + # we are called as well in other cases but we do not need to + # act here as we don't display signal strength for Ad-hoc networks + return + if old_hash is None: # new AP finished initializing self._add_ap_to_network(ap) return @@ -923,6 +1310,11 @@ class MeshBox(gtk.VBox): ap.initialize() def remove_access_point(self, ap_o): + if self._adhoc_manager is not None: + if self._adhoc_manager.is_sugar_adhoc_access_point(ap_o): + self._adhoc_manager.remove_access_point(ap_o) + return + # we don't keep an index of ap object path to network, but since # we'll only ever have a handful of networks, just try them all... for net in self.wireless_networks.values(): @@ -935,18 +1327,68 @@ class MeshBox(gtk.VBox): self._remove_net_if_empty(net, ap.network_hash()) return - logging.error('Can not remove access point %s', ap_o) + # it's not an error if the AP isn't found, since we might have ignored + # it (e.g. olpc-mesh adhoc network) + logging.debug('Can not remove access point %s' % ap_o) + + def add_adhoc_networks(self, device): + if self._adhoc_manager is None: + self._adhoc_manager = get_adhoc_manager_instance() + self._adhoc_manager.start_listening(device) + self._add_adhoc_network_icon(1) + self._add_adhoc_network_icon(6) + self._add_adhoc_network_icon(11) + self._adhoc_manager.autoconnect() + + def remove_adhoc_networks(self): + for icon in self._adhoc_networks: + self._layout.remove(icon) + self._adhoc_networks = [] + + def _add_adhoc_network_icon(self, channel): + icon = SugarAdhocView(channel) + self._layout.add(icon) + self._adhoc_networks.append(icon) + + def _add_olpc_mesh_icon(self, mesh_mgr, channel): + icon = OlpcMeshView(mesh_mgr, channel) + self._layout.add(icon) + self._mesh.append(icon) + + def enable_olpc_mesh(self, mesh_device): + mesh_mgr = OlpcMeshManager(mesh_device) + self._add_olpc_mesh_icon(mesh_mgr, 1) + self._add_olpc_mesh_icon(mesh_mgr, 6) + self._add_olpc_mesh_icon(mesh_mgr, 11) + + # the OLPC mesh can be recognised as a "normal" wifi network. remove + # any such normal networks if they have been created + for hash, net in self.wireless_networks.iteritems(): + if not net.is_olpc_mesh(): + continue + + logging.debug("removing OLPC mesh IBSS") + net.remove_all_aps() + net.disconnect() + self._layout.remove(net) + del self.wireless_networks[hash] + + def disable_olpc_mesh(self, mesh_device): + for icon in self._mesh: + icon.disconnect() + self._layout.remove(icon) + self._mesh = [] def suspend(self): if not self._suspended: self._suspended = True - for net in self.wireless_networks.values(): + for net in self.wireless_networks.values() + self._mesh: net.props.paused = True def resume(self): if self._suspended: self._suspended = False - for net in self.wireless_networks.values(): + for net in self.wireless_networks.values() + self._mesh: net.props.paused = False def _toolbar_query_changed_cb(self, toolbar, query): diff --git a/src/jarabe/desktop/schoolserver.py b/src/jarabe/desktop/schoolserver.py index 1dd9edc..a7d0e63 100644 --- a/src/jarabe/desktop/schoolserver.py +++ b/src/jarabe/desktop/schoolserver.py @@ -20,6 +20,7 @@ from xmlrpclib import ServerProxy, Error import socket import os import gconf +import dbus from sugar.profile import get_profile @@ -57,6 +58,8 @@ def register_laptop(url=REGISTER_URL): client.set_string('/desktop/sugar/collaboration/jabber_server', data['jabberserver']) + _restart_jabber() + client.set_string('/desktop/sugar/backup_url', data['backupurl']) return True @@ -72,3 +75,19 @@ def read_ofw(path): data = fh.read().rstrip('\0\n') fh.close() return data + +def _restart_jabber(): + """Call Sugar Presence Service to restart Telepathy CMs. + + This allows restarting the jabber server connection when we change it. + """ + _PS_SERVICE = "org.laptop.Sugar.Presence" + _PS_INTERFACE = "org.laptop.Sugar.Presence" + _PS_PATH = "/org/laptop/Sugar/Presence" + bus = dbus.SessionBus() + try: + ps = dbus.Interface(bus.get_object(_PS_SERVICE, _PS_PATH), + _PS_INTERFACE) + except dbus.DBusException: + raise RegisterError('%s service not available' % _PS_SERVICE) + ps.RetryConnections() diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py index 18cc64a..657f03c 100644 --- a/src/jarabe/journal/journalactivity.py +++ b/src/jarabe/journal/journalactivity.py @@ -27,6 +27,9 @@ import statvfs import os from sugar.graphics.window import Window +from sugar.graphics.alert import Alert +from sugar.graphics.icon import Icon + from sugar.bundle.bundle import ZipExtractException, RegistrationException from sugar import env from sugar.activity import activityfactory @@ -138,6 +141,18 @@ class JournalActivity(Window): self._critical_space_alert = None self._check_available_space() + def __volume_error_cb(self, gobject, message, severity): + alert = Alert(title=severity, msg=message) + icon = Icon(icon_name='dialog-ok') + alert.add_button(gtk.RESPONSE_OK, _('Ok'), icon) + icon.show() + alert.connect('response', self.__alert_response_cb) + self.add_alert(alert) + alert.show() + + def __alert_response_cb(self, alert, response_id): + self.remove_alert(alert) + def __realize_cb(self, window): wm.set_bundle_id(window.window, _BUNDLE_ID) activity_id = activityfactory.create_activity_id() @@ -161,6 +176,8 @@ class JournalActivity(Window): self._volumes_toolbar = VolumesToolbar() self._volumes_toolbar.connect('volume-changed', self.__volume_changed_cb) + self._volumes_toolbar.connect('volume-error', + self.__volume_error_cb) self._main_view.pack_start(self._volumes_toolbar, expand=False) search_toolbar = self._main_toolbox.search_toolbar @@ -171,8 +188,8 @@ class JournalActivity(Window): self._secondary_view = gtk.VBox() self._detail_toolbox = DetailToolbox() - entry_toolbar = self._detail_toolbox.entry_toolbar - + self._detail_toolbox.entry_toolbar.connect('volume-error', + self.__volume_error_cb) self._detail_view = DetailView() self._detail_view.connect('go-back-clicked', self.__go_back_clicked_cb) self._secondary_view.pack_end(self._detail_view) @@ -180,8 +197,6 @@ class JournalActivity(Window): def _key_press_event_cb(self, widget, event): keyname = gtk.gdk.keyval_name(event.keyval) - logging.info(keyname) - logging.info(event.state) if keyname == 'Escape': self.show_main_view() diff --git a/src/jarabe/journal/journalentrybundle.py b/src/jarabe/journal/journalentrybundle.py index 9e68c06..41777c7 100644 --- a/src/jarabe/journal/journalentrybundle.py +++ b/src/jarabe/journal/journalentrybundle.py @@ -40,7 +40,7 @@ class JournalEntryBundle(Bundle): def __init__(self, path): Bundle.__init__(self, path) - def install(self, install_path, uid=''): + def install(self, uid=''): if os.environ.has_key('SUGAR_ACTIVITY_ROOT'): install_dir = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'], 'data') diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py index 17a65e6..f71049e 100644 --- a/src/jarabe/journal/journaltoolbox.py +++ b/src/jarabe/journal/journaltoolbox.py @@ -325,6 +325,11 @@ class DetailToolbox(Toolbox): self.entry_toolbar.show() class EntryToolbar(gtk.Toolbar): + __gsignals__ = { + 'volume-error': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str, str])) + } def __init__(self): gtk.Toolbar.__init__(self) @@ -394,7 +399,22 @@ class EntryToolbar(gtk.Toolbar): misc.resume(self._metadata, service_name) def _copy_menu_item_activate_cb(self, menu_item, mount): - model.copy(self._metadata, mount.get_root().get_path()) + file_path = model.get_file(self._metadata['uid']) + + if not file_path or not os.path.exists(file_path): + logging.warn('Entries without a file cannot be copied.') + self.emit('volume-error', + _('Entries without a file cannot be copied.'), + _('Warning')) + return + + try: + model.copy(self._metadata, mount.get_root().get_path()) + except (IOError, OSError), e: + logging.exception('Error while copying the entry. %s', e.strerror) + self.emit('volume-error', + _('Error while copying the entry. %s') % e.strerror, + _('Error')) def _refresh_copy_palette(self): palette = self._copy.get_palette() diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py index 6556b08..e1ca620 100644 --- a/src/jarabe/journal/listview.py +++ b/src/jarabe/journal/listview.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007, One Laptop Per Child +# Copyright (C) 2007, 2010 One Laptop Per Child # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -23,7 +23,6 @@ import time import hippo import gobject import gtk -import dbus from sugar.graphics import style from sugar.graphics.icon import CanvasIcon, Icon @@ -31,10 +30,6 @@ from sugar.graphics.icon import CanvasIcon, Icon from jarabe.journal.collapsedentry import CollapsedEntry from jarabe.journal import model -DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' -DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' -DS_DBUS_PATH = '/org/laptop/sugar/DataStore' - UPDATE_INTERVAL = 300 EMPTY_JOURNAL = _("Your Journal is empty") @@ -109,19 +104,18 @@ class BaseListView(gtk.HBox): self._refresh_idle_handler = None self._update_dates_timer = None - bus = dbus.SessionBus() - datastore = dbus.Interface( - bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH), DS_DBUS_INTERFACE) - self._datastore_created_handler = \ - datastore.connect_to_signal('Created', - self.__datastore_created_cb) - self._datastore_updated_handler = \ - datastore.connect_to_signal('Updated', - self.__datastore_updated_cb) + model.created.connect(self.__model_created_cb) + model.updated.connect(self.__model_updated_cb) + model.deleted.connect(self.__model_deleted_cb) + + def __model_created_cb(self, sender, **kwargs): + self._set_dirty() + + def __model_updated_cb(self, sender, **kwargs): + self._set_dirty() - self._datastore_deleted_handler = \ - datastore.connect_to_signal('Deleted', - self.__datastore_deleted_cb) + def __model_deleted_cb(self, sender, **kwargs): + self._set_dirty() def __destroy_cb(self, widget): self._datastore_created_handler.remove() @@ -463,15 +457,6 @@ class BaseListView(gtk.HBox): if entry.get_visible(): entry.update_date() - def __datastore_created_cb(self, uid): - self._set_dirty() - - def __datastore_updated_cb(self, uid): - self._set_dirty() - - def __datastore_deleted_cb(self, uid): - self._set_dirty() - def _set_dirty(self): if self._fully_obscured: self._dirty = True diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py index b29b744..890fe60 100644 --- a/src/jarabe/journal/misc.py +++ b/src/jarabe/journal/misc.py @@ -95,21 +95,21 @@ def get_date(metadata): def get_bundle(metadata): try: if is_activity_bundle(metadata): - file_path = util.TempFilePath(model.get_file(metadata['uid'])) + file_path = model.get_file(metadata['uid']) if not os.path.exists(file_path): logging.warning('Invalid path: %r' % file_path) return None return ActivityBundle(file_path) elif is_content_bundle(metadata): - file_path = util.TempFilePath(model.get_file(metadata['uid'])) + file_path = model.get_file(metadata['uid']) if not os.path.exists(file_path): logging.warning('Invalid path: %r' % file_path) return None return ContentBundle(file_path) elif is_journal_bundle(metadata): - file_path = util.TempFilePath(model.get_file(metadata['uid'])) + file_path = model.get_file(metadata['uid']) if not os.path.exists(file_path): logging.warning('Invalid path: %r' % file_path) return None diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py index 1b4e236..a93321e 100644 --- a/src/jarabe/journal/model.py +++ b/src/jarabe/journal/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007-2008, One Laptop Per Child +# Copyright (C) 2007, 2008, 2010 One Laptop Per Child # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,16 +16,18 @@ import logging import os +import errno from datetime import datetime import time import shutil -from stat import S_IFMT, S_IFDIR, S_IFREG -import traceback +import tempfile +from stat import S_IFLNK, S_IFMT, S_IFDIR, S_IFREG import re +import json +from gettext import gettext as _ import gobject import dbus -import gconf import gio from sugar import dispatch @@ -43,6 +45,8 @@ PROPERTIES = ['uid', 'title', 'mtime', 'timestamp', 'keep', 'buddies', PAGES_TO_CACHE = 5 +JOURNAL_METADATA_DIR = '.Sugar-Metadata' + class _Cache(object): __gtype_name__ = 'model_Cache' @@ -258,7 +262,9 @@ class InplaceResultSet(BaseResultSet): BaseResultSet.__init__(self, query, cache_limit) self._mount_point = mount_point self._file_list = None - self._pending_directories = 0 + self._pending_directories = [] + self._visited_directories = [] + self._pending_files = [] self._stopped = False query_text = query.get('query', '') @@ -283,7 +289,10 @@ class InplaceResultSet(BaseResultSet): def setup(self): self._file_list = [] - self._recurse_dir(self._mount_point) + self._pending_directories = [self._mount_point] + self._visited_directories = [] + self._pending_files = [] + gobject.idle_add(self._scan) def stop(self): self._stopped = True @@ -308,8 +317,9 @@ class InplaceResultSet(BaseResultSet): files = self._file_list[offset:offset + limit] entries = [] - for file_path, stat, mtime_ in files: - metadata = _get_file_metadata(file_path, stat) + for file_path, stat, mtime_, metadata in files: + if metadata is None: + metadata = _get_file_metadata(file_path, stat) metadata['mountpoint'] = self._mount_point entries.append(metadata) @@ -317,63 +327,166 @@ class InplaceResultSet(BaseResultSet): return entries, total_count - def _recurse_dir(self, dir_path): + def _scan(self): if self._stopped: + return False + + self.progress.send(self) + + if self._pending_files: + self._scan_a_file() + return True + + if self._pending_directories: + self._scan_a_directory() + return True + + self.setup_ready() + self._visited_directories = [] + return False + + def _scan_a_file(self): + full_path = self._pending_files.pop(0) + metadata = None + + try: + stat = os.lstat(full_path) + except OSError, e: + if e.errno != errno.ENOENT: + logging.exception( + 'Error reading metadata of file %r', full_path) return - for entry in os.listdir(dir_path): - if entry.startswith('.'): - continue - full_path = dir_path + '/' + entry + if S_IFMT(stat.st_mode) == S_IFLNK: + try: + link = os.readlink(full_path) + except OSError, e: + logging.exception( + 'Error reading target of link %r', full_path) + return + + if not os.path.abspath(link).startswith(self._mount_point): + return + try: stat = os.stat(full_path) - if S_IFMT(stat.st_mode) == S_IFDIR: - self._pending_directories += 1 - gobject.idle_add(lambda s=full_path: self._recurse_dir(s)) - elif S_IFMT(stat.st_mode) == S_IFREG: - add_to_list = True + except OSError, e: + if e.errno != errno.ENOENT: + logging.exception( + 'Error reading metadata of linked file %r', full_path) + return + + if S_IFMT(stat.st_mode) == S_IFDIR: + id_tuple = stat.st_ino, stat.st_dev + if not id_tuple in self._visited_directories: + self._visited_directories.append(id_tuple) + self._pending_directories.append(full_path) + return + + if S_IFMT(stat.st_mode) != S_IFREG: + return - if self._regex is not None and \ - not self._regex.match(full_path): - add_to_list = False + if self._regex is not None and \ + not self._regex.match(full_path): + filename = os.path.basename(full_path) + dir_path = os.path.dirname(full_path) + metadata = _get_file_metadata_from_json( \ + dir_path, filename, preview=False) + add_to_list = False + if metadata is not None: + for f in ['fulltext', 'title', + 'description', 'tags']: + if f in metadata and \ + self._regex.match(metadata[f]): + add_to_list = True + break + if not add_to_list: + return + + if self._date_start is not None and stat.st_mtime < self._date_start: + return - if None not in [self._date_start, self._date_end] and \ - (stat.st_mtime < self._date_start or - stat.st_mtime > self._date_end): - add_to_list = False + if self._date_end is not None and stat.st_mtime > self._date_end: + return - if self._mime_types: - mime_type = gio.content_type_guess(filename=full_path) - if mime_type not in self._mime_types: - add_to_list = False + if self._mime_types: + mime_type = gio.content_type_guess(filename=full_path) + if mime_type not in self._mime_types: + return - if add_to_list: - file_info = (full_path, stat, int(stat.st_mtime)) - self._file_list.append(file_info) + file_info = (full_path, stat, int(stat.st_mtime), metadata) + self._file_list.append(file_info) - self.progress.send(self) + return - except Exception: - logging.error('Error reading file %r: %s' % \ - (full_path, traceback.format_exc())) + def _scan_a_directory(self): + dir_path = self._pending_directories.pop(0) - if self._pending_directories == 0: - self.setup_ready() - else: - self._pending_directories -= 1 + try: + entries = os.listdir(dir_path) + except OSError, e: + if e.errno != errno.EACCES: + logging.exception('Error reading directory %r', dir_path) + return + + for entry in entries: + if entry.startswith('.'): + continue + self._pending_files.append(dir_path + '/' + entry) + return def _get_file_metadata(path, stat): - client = gconf.client_get_default() + """Returns the metadata from the corresponding file + on the external device or does create the metadata + based on the file properties. + + """ + filename = os.path.basename(path) + dir_path = os.path.dirname(path) + metadata = _get_file_metadata_from_json(dir_path, filename, preview=True) + if metadata: + return metadata + return {'uid': path, 'title': os.path.basename(path), 'timestamp': stat.st_mtime, 'mime_type': gio.content_type_guess(filename=path), 'activity': '', 'activity_id': '', - 'icon-color': client.get_string('/desktop/sugar/user/color'), + 'icon-color': '', 'description': path} +def _get_file_metadata_from_json(dir_path, filename, preview=False): + """Returns the metadata from the json file and the preview + stored on the external device. + + """ + metadata = None + metadata_path = os.path.join(dir_path, JOURNAL_METADATA_DIR, + filename + '.metadata') + if os.path.exists(metadata_path): + try: + metadata = json.load(open(metadata_path)) + except ValueError: + logging.debug("Could not read metadata for file %r on" \ + "external device.", filename) + else: + metadata['uid'] = os.path.join(dir_path, filename) + if preview: + preview_path = os.path.join(dir_path, JOURNAL_METADATA_DIR, + filename + '.preview') + if os.path.exists(preview_path): + try: + metadata['preview'] = dbus.ByteArray(open(preview_path).read()) + except: + logging.debug("Could not read preview for file %r on" \ + "external device.", filename) + else: + if metadata and 'preview' in metadata: + del(metadata['preview']) + return metadata + _datastore = None def _get_datastore(): global _datastore @@ -460,6 +573,19 @@ def delete(object_id): """ if os.path.exists(object_id): os.unlink(object_id) + dir_path = os.path.dirname(object_id) + filename = os.path.basename(object_id) + old_files = [os.path.join(dir_path, JOURNAL_METADATA_DIR, + filename + '.metadata'), + os.path.join(dir_path, JOURNAL_METADATA_DIR, + filename + '.preview')] + for old_file in old_files: + if os.path.exists(old_file): + try: + os.unlink(old_file) + except: + pass + deleted.send(None, object_id=object_id) else: _get_datastore().delete(object_id) @@ -472,9 +598,9 @@ def copy(metadata, mount_point): metadata['mountpoint'] = mount_point del metadata['uid'] - return write(metadata, file_path) + return write(metadata, file_path, transfer_ownership=False) -def write(metadata, file_path='', update_mtime=True): +def write(metadata, file_path='', update_mtime=True, transfer_ownership=True): """Creates or updates an entry for that id """ logging.debug('model.write %r %r %r' % (metadata.get('uid', ''), file_path, @@ -488,31 +614,110 @@ def write(metadata, file_path='', update_mtime=True): object_id = _get_datastore().update(metadata['uid'], dbus.Dictionary(metadata), file_path, - True) + transfer_ownership) else: object_id = _get_datastore().create(dbus.Dictionary(metadata), file_path, - True) + transfer_ownership) else: - if not os.path.exists(file_path): - raise ValueError('Entries without a file cannot be copied to ' - 'removable devices') + object_id = _write_entry_on_external_device(metadata, file_path) - file_name = _get_file_name(metadata['title'], metadata['mime_type']) - file_name = _get_unique_file_name(metadata['mountpoint'], file_name) + return object_id + +def _write_entry_on_external_device(metadata, file_path): + """This creates and updates an entry copied from the + DS to external storage device. Besides copying the + associated file a hidden file for the preview and one + for the metadata are stored. We make sure that the + metadata and preview file are in the same directory + as the data file. + + This function handles renames of an entry on the + external device and avoids name collisions. Renames are + handled failsafe. + + """ + if 'uid' in metadata and os.path.exists(metadata['uid']): + file_path = metadata['uid'] + + if not file_path or not os.path.exists(file_path): + raise ValueError('Entries without a file cannot be copied to ' + 'removable devices') + if metadata['title'] == '': + metadata['title'] = _('Untitled') + file_name = get_file_name(metadata['title'], metadata['mime_type']) + + destination_path = os.path.join(metadata['mountpoint'], file_name) + if destination_path != file_path: + file_name = get_unique_file_name(metadata['mountpoint'], file_name) destination_path = os.path.join(metadata['mountpoint'], file_name) + clean_name, extension_ = os.path.splitext(file_name) + metadata['title'] = clean_name + + metadata_copy = metadata.copy() + del metadata_copy['mountpoint'] + if 'uid' in metadata_copy: + del metadata_copy['uid'] + + metadata_dir_path = os.path.join(metadata['mountpoint'], + JOURNAL_METADATA_DIR) + if not os.path.exists(metadata_dir_path): + os.mkdir(metadata_dir_path) + + if 'preview' in metadata_copy: + preview = metadata_copy['preview'] + preview_fname = file_name + '.preview' + preview_path = os.path.join(metadata['mountpoint'], + JOURNAL_METADATA_DIR, preview_fname) + metadata_copy['preview'] = preview_fname + + (fh, fn) = tempfile.mkstemp(dir=metadata['mountpoint']) + os.write(fh, preview) + os.close(fh) + os.rename(fn, preview_path) + + metadata_path = os.path.join(metadata['mountpoint'], + JOURNAL_METADATA_DIR, + file_name + '.metadata') + (fh, fn) = tempfile.mkstemp(dir=metadata['mountpoint']) + os.write(fh, json.dumps(metadata_copy)) + os.close(fh) + os.rename(fn, metadata_path) + + if os.path.dirname(destination_path) == os.path.dirname(file_path): + old_file_path = file_path + if old_file_path != destination_path: + os.rename(file_path, destination_path) + old_fname = os.path.basename(file_path) + old_files = [os.path.join(metadata['mountpoint'], + JOURNAL_METADATA_DIR, + old_fname + '.metadata'), + os.path.join(metadata['mountpoint'], + JOURNAL_METADATA_DIR, + old_fname + '.preview')] + for ofile in old_files: + if os.path.exists(ofile): + try: + os.unlink(ofile) + except: + pass + else: shutil.copy(file_path, destination_path) - object_id = destination_path + + object_id = destination_path + created.send(None, object_id=object_id) return object_id -def _get_file_name(title, mime_type): +def get_file_name(title, mime_type): file_name = title - extension = '.' + mime.get_primary_extension(mime_type) - if not file_name.endswith(extension): - file_name += extension + mime_extension = mime.get_primary_extension(mime_type) + if mime_extension: + extension = '.' + mime_extension + if not file_name.endswith(extension): + file_name += extension # Invalid characters in VFAT filenames. From # http://en.wikipedia.org/wiki/File_Allocation_Table @@ -529,11 +734,11 @@ def _get_file_name(title, mime_type): return file_name -def _get_unique_file_name(mount_point, file_name): +def get_unique_file_name(mount_point, file_name): if os.path.exists(os.path.join(mount_point, file_name)): i = 1 + name, extension = os.path.splitext(file_name) while len(file_name) <= 255: - name, extension = os.path.splitext(file_name) file_name = name + '_' + str(i) + extension if not os.path.exists(os.path.join(mount_point, file_name)): break diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py index 2c15591..c16f374 100644 --- a/src/jarabe/journal/palettes.py +++ b/src/jarabe/journal/palettes.py @@ -68,22 +68,29 @@ class ObjectPalette(Palette): Palette.__init__(self, primary_text=title, icon=activity_icon) - if metadata.get('activity_id', ''): - resume_label = _('Resume') - resume_with_label = _('Resume with') - else: - resume_label = _('Start') - resume_with_label = _('Start with') - menu_item = MenuItem(resume_label, 'activity-start') - menu_item.connect('activate', self.__start_activate_cb) - self.menu.append(menu_item) - menu_item.show() + if misc.get_activities(metadata) or misc.is_bundle(metadata): + if metadata.get('activity_id', ''): + resume_label = _('Resume') + resume_with_label = _('Resume with') + else: + resume_label = _('Start') + resume_with_label = _('Start with') + menu_item = MenuItem(resume_label, 'activity-start') + menu_item.connect('activate', self.__start_activate_cb) + self.menu.append(menu_item) + menu_item.show() - menu_item = MenuItem(resume_with_label, 'activity-start') - self.menu.append(menu_item) - menu_item.show() - start_with_menu = StartWithMenu(self._metadata) - menu_item.set_submenu(start_with_menu) + menu_item = MenuItem(resume_with_label, 'activity-start') + self.menu.append(menu_item) + menu_item.show() + start_with_menu = StartWithMenu(self._metadata) + menu_item.set_submenu(start_with_menu) + + else: + menu_item = MenuItem(_('No activity to start entry')) + menu_item.set_sensitive(False) + self.menu.append(menu_item) + menu_item.show() client = gconf.client_get_default() color = XoColor(client.get_string('/desktop/sugar/user/color')) @@ -134,11 +141,6 @@ class ObjectPalette(Palette): self._temp_file_path = None def __erase_activate_cb(self, menu_item): - registry = bundleregistry.get_registry() - - bundle = misc.get_bundle(self._metadata) - if bundle is not None and registry.is_installed(bundle): - registry.uninstall(bundle) model.delete(self._metadata['uid']) def __detail_activate_cb(self, menu_item): diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py index b21832e..9a49cdf 100644 --- a/src/jarabe/journal/volumestoolbar.py +++ b/src/jarabe/journal/volumestoolbar.py @@ -15,7 +15,13 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import logging +import os from gettext import gettext as _ +import cPickle +import xapian +import json +import tempfile +import shutil import gobject import gio @@ -29,13 +35,126 @@ from sugar.graphics.xocolor import XoColor from jarabe.journal import model from jarabe.view.palettes import VolumePalette +_JOURNAL_0_METADATA_DIR = '.olpc.store' + +def _get_id(document): + """Get the ID for the document in the xapian database.""" + tl = document.termlist() + try: + term = tl.skip_to('Q').term + if len(term) == 0 or term[0] != 'Q': + return None + return term[1:] + except StopIteration: + return None + +def _convert_entries(root): + """Converts the entries written by the datastore version 0. + The metadata and the preview will be written using the new + scheme for writing Journal entries to removable storage + devices. + + - entries that do not have an associated file are not + converted. + - if an entry has no title we set it to Untitled and rename + the file accordingly, taking care of creating a unique + filename + + """ + try: + database = xapian.Database(os.path.join(root, _JOURNAL_0_METADATA_DIR, + 'index')) + except xapian.DatabaseError, e: + logging.error('Convert DS-0 Journal entry. Error reading db: %s', + os.path.join(root, _JOURNAL_0_METADATA_DIR, 'index')) + return + + metadata_dir_path = os.path.join(root, model.JOURNAL_METADATA_DIR) + if not os.path.exists(metadata_dir_path): + os.mkdir(metadata_dir_path) + + for i in range(1, database.get_lastdocid() + 1): + try: + document = database.get_document(i) + except xapian.DocNotFoundError, e: + logging.debug('Convert DS-0 Journal entry. ' \ + 'Error getting document %s: %s', i, e) + continue + + try: + metadata_loaded = cPickle.loads(document.get_data()) + except cPickle.PickleError, e: + logging.debug('Convert DS-0 Journal entry. ' \ + 'Error converting metadata: %s', e) + continue + + if 'activity_id' in metadata_loaded and \ + 'mime_type' in metadata_loaded and \ + 'title' in metadata_loaded: + metadata = {} + + uid = _get_id(document) + if uid is None: + continue + + for key, value in metadata_loaded.items(): + metadata[str(key)] = str(value[0]) + + if 'uid' not in metadata: + metadata['uid'] = uid + + if 'filename' in metadata: + filename = metadata['filename'] + else: + continue + if not os.path.exists(os.path.join(root, filename)): + continue + + if metadata['title'] == '': + metadata['title'] = _('Untitled') + fn = model.get_file_name(metadata['title'], + metadata['mime_type']) + new_filename = model.get_unique_file_name(root, fn) + metadata['filename'] = new_filename + os.rename(os.path.join(root, filename), + os.path.join(root, new_filename)) + filename = new_filename + + preview_path = os.path.join(root, _JOURNAL_0_METADATA_DIR, + 'preview', uid) + if os.path.exists(preview_path): + preview_fname = filename + '.preview' + new_preview_path = os.path.join(root, + model.JOURNAL_METADATA_DIR, + preview_fname) + if not os.path.exists(new_preview_path): + metadata['preview'] = preview_fname + shutil.copy(preview_path, new_preview_path) + + metadata_fname = filename + '.metadata' + metadata_path = os.path.join(root, model.JOURNAL_METADATA_DIR, + metadata_fname) + if not os.path.exists(metadata_path): + (fh, fn) = tempfile.mkstemp(dir=root) + os.write(fh, json.dumps(metadata)) + os.close(fh) + os.rename(fn, metadata_path) + + logging.debug('Convert DS-0 Journal entry. Entry converted: ' \ + 'File=%s Metadata=%s', + os.path.join(root, filename), metadata) + + class VolumesToolbar(gtk.Toolbar): __gtype_name__ = 'VolumesToolbar' __gsignals__ = { 'volume-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([str])) + ([str])), + 'volume-error': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str, str])) } def __init__(self): @@ -78,9 +197,15 @@ class VolumesToolbar(gtk.Toolbar): def _add_button(self, mount): logging.debug('VolumeToolbar._add_button: %r' % mount.get_name()) + if os.path.exists(os.path.join(mount.get_root().get_path(), + _JOURNAL_0_METADATA_DIR)): + logging.debug('Convert DS-0 Journal entries.') + gobject.idle_add(_convert_entries, mount.get_root().get_path()) + button = VolumeButton(mount) button.props.group = self._volume_buttons[0] button.connect('toggled', self._button_toggled_cb) + button.connect('volume-error', self.__volume_error_cb) position = self.get_item_index(self._volume_buttons[-1]) + 1 self.insert(button, position) button.show() @@ -90,6 +215,9 @@ class VolumesToolbar(gtk.Toolbar): if len(self.get_children()) > 1: self.show() + def __volume_error_cb(self, button, strerror, severity): + self.emit('volume-error', strerror, severity) + def _button_toggled_cb(self, button): if button.props.active: self.emit('volume-changed', button.mount_point) @@ -123,6 +251,12 @@ class VolumesToolbar(gtk.Toolbar): button.props.active = True class BaseButton(RadioToolButton): + __gsignals__ = { + 'volume-error': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str, str])) + } + def __init__(self, mount_point): RadioToolButton.__init__(self) @@ -137,7 +271,22 @@ class BaseButton(RadioToolButton): info, timestamp): object_id = selection_data.data metadata = model.get(object_id) - model.copy(metadata, self.mount_point) + file_path = model.get_file(metadata['uid']) + + if not file_path or not os.path.exists(file_path): + logging.warn('Entries without a file cannot be copied.') + self.emit('volume-error', + _('Entries without a file cannot be copied.'), + _('Warning')) + return + + try: + model.copy(metadata, self.mount_point) + except (IOError, OSError), e: + logging.exception('Error while copying the entry. %s', e.strerror) + self.emit('volume-error', + _('Error while copying the entry. %s') % e.strerror, + _('Error')) class VolumeButton(BaseButton): def __init__(self, mount): diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am index 399db65..1df2cde 100644 --- a/src/jarabe/model/Makefile.am +++ b/src/jarabe/model/Makefile.am @@ -1,11 +1,13 @@ sugardir = $(pythondir)/jarabe/model sugar_PYTHON = \ + adhoc.py \ __init__.py \ buddy.py \ bundleregistry.py \ filetransfer.py \ friends.py \ invites.py \ + olpcmesh.py \ owner.py \ neighborhood.py \ network.py \ diff --git a/src/jarabe/model/adhoc.py b/src/jarabe/model/adhoc.py new file mode 100644 index 0000000..5c9d6f5 --- /dev/null +++ b/src/jarabe/model/adhoc.py @@ -0,0 +1,280 @@ +# Copyright (C) 2010 One Laptop per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +import gobject + +from jarabe.model import network +from jarabe.model.network import Settings +from sugar.util import unique_id +from jarabe.model.network import IP4Config + +_NM_SERVICE = 'org.freedesktop.NetworkManager' +_NM_IFACE = 'org.freedesktop.NetworkManager' +_NM_PATH = '/org/freedesktop/NetworkManager' +_NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' +_NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' +_NM_ACCESSPOINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' +_NM_ACTIVE_CONN_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' + + +_adhoc_manager_instance = None +def get_adhoc_manager_instance(): + global _adhoc_manager_instance + if _adhoc_manager_instance is None: + _adhoc_manager_instance = AdHocManager() + return _adhoc_manager_instance + + +class AdHocManager(gobject.GObject): + """To mimic the mesh behavior on devices where mesh hardware is + not available we support the creation of an Ad-hoc network on + three channels 1, 6, 11. If Sugar sees no "known" network when it + starts, it does autoconnect to an Ad-hoc network. + + """ + + __gsignals__ = { + 'members-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + 'state-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])) + } + + _AUTOCONNECT_TIMEOUT = 30 + _CHANNEL_1 = 1 + _CHANNEL_6 = 6 + _CHANNEL_11 = 11 + + def __init__(self): + gobject.GObject.__init__(self) + + self._bus = dbus.SystemBus() + self._device = None + self._idle_source = 0 + self._listening_called = 0 + self._device_state = network.DEVICE_STATE_UNKNOWN + + self._current_channel = None + self._networks = {self._CHANNEL_1: None, + self._CHANNEL_6: None, + self._CHANNEL_11: None} + + def start_listening(self, device): + self._listening_called += 1 + if self._listening_called > 1: + raise RuntimeError('The start listening method can' \ + ' only be called once.') + + self._device = device + props = dbus.Interface(device, 'org.freedesktop.DBus.Properties') + self._device_state = props.Get(_NM_DEVICE_IFACE, 'State') + + self._bus.add_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + + self._bus.add_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._device.object_path, + dbus_interface=_NM_WIRELESS_IFACE) + + def stop_listening(self): + self._bus.remove_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + self._bus.remove_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._device.object_path, + dbus_interface=_NM_WIRELESS_IFACE) + + def __device_state_changed_cb(self, new_state, old_state, reason): + self._device_state = new_state + self._update_state() + + def __wireless_properties_changed_cb(self, properties): + if 'ActiveAccessPoint' in properties and \ + properties['ActiveAccessPoint'] != '/': + active_ap = self._bus.get_object(_NM_SERVICE, + properties['ActiveAccessPoint']) + props = dbus.Interface(active_ap, dbus.PROPERTIES_IFACE) + props.GetAll(_NM_ACCESSPOINT_IFACE, byte_arrays=True, + reply_handler=self.__get_all_ap_props_reply_cb, + error_handler=self.__get_all_ap_props_error_cb) + + def __get_all_ap_props_reply_cb(self, properties): + if properties['Mode'] == network.NM_802_11_MODE_ADHOC and \ + 'Frequency' in properties: + frequency = properties['Frequency'] + self._current_channel = network.frequency_to_channel(frequency) + else: + self._current_channel = None + self._update_state() + + def __get_all_ap_props_error_cb(self, err): + logging.error('Error getting the access point properties: %s', err) + + def _update_state(self): + self.emit('state-changed', self._current_channel, self._device_state) + + def autoconnect(self): + """Start a timer which basically looks for 30 seconds of inactivity + on the device, then does autoconnect to an Ad-hoc network. + + """ + if self._idle_source != 0: + gobject.source_remove(self._idle_source) + self._idle_source = gobject.timeout_add_seconds( \ + self._AUTOCONNECT_TIMEOUT, self.__idle_check_cb) + + def __idle_check_cb(self): + if self._device_state == network.DEVICE_STATE_DISCONNECTED: + logging.debug("Connect to Ad-hoc network due to inactivity.") + self._autoconnect_adhoc() + return False + + def _autoconnect_adhoc(self): + """First we try if there is an Ad-hoc network that is used by other + learners in the area, if not we default to channel 1. + + """ + if self._networks[self._CHANNEL_1] is not None: + self._connect(self._CHANNEL_1) + elif self._networks[self._CHANNEL_6] is not None: + self._connect(self._CHANNEL_6) + elif self._networks[self._CHANNEL_11] is not None: + self._connect(self._CHANNEL_11) + else: + self._connect(self._CHANNEL_1) + + def activate_channel(self, channel): + """Activate a sugar Ad-hoc network. + + Keyword arguments: + channel -- Channel to connect to (should be 1, 6, 11) + + """ + self._connect(channel) + + def _connect(self, channel): + name = "Ad-hoc Network %d" % channel + connection = network.find_connection_by_ssid(name) + if connection is None: + settings = Settings() + settings.connection.id = name + settings.connection.uuid = unique_id() + settings.connection.type = '802-11-wireless' + settings.wireless.ssid = dbus.ByteArray(name) + settings.wireless.band = 'bg' + settings.wireless.channel = channel + settings.wireless.mode = 'adhoc' + settings.ip4_config = IP4Config() + settings.ip4_config.method = 'link-local' + + connection = network.add_connection(name, settings) + + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + + netmgr.ActivateConnection(network.SETTINGS_SERVICE, + connection.path, + self._device.object_path, + '/', + reply_handler=self.__activate_reply_cb, + error_handler=self.__activate_error_cb) + + def deactivate_active_channel(self): + """Deactivate the current active channel.""" + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + + netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE) + netmgr_props.Get(_NM_IFACE, 'ActiveConnections', \ + reply_handler=self.__get_active_connections_reply_cb, + error_handler=self.__get_active_connections_error_cb) + + def __get_active_connections_reply_cb(self, active_connections_o): + for connection_o in active_connections_o: + obj = self._bus.get_object(_NM_IFACE, connection_o) + props = dbus.Interface(obj, dbus.PROPERTIES_IFACE) + state = props.Get(_NM_ACTIVE_CONN_IFACE, 'State') + if state == network.NM_ACTIVE_CONNECTION_STATE_ACTIVATED: + access_point_o = props.Get(_NM_ACTIVE_CONN_IFACE, + 'SpecificObject') + if access_point_o != '/': + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + netmgr.DeactivateConnection(connection_o) + + def __get_active_connections_error_cb(self, err): + logging.error('Error getting the active connections: %s', err) + + def __activate_reply_cb(self, connection): + logging.debug('Ad-hoc network created: %s', connection) + + def __activate_error_cb(self, err): + logging.error('Failed to create Ad-hoc network: %s', err) + + def add_access_point(self, access_point): + """Add an access point to a network and notify the view to idicate + the member change. + + Keyword arguments: + access_point -- Access Point + + """ + if access_point.name.endswith(' 1'): + self._networks[self._CHANNEL_1] = access_point + self.emit('members-changed', self._CHANNEL_1, True) + elif access_point.name.endswith(' 6'): + self._networks[self._CHANNEL_6] = access_point + self.emit('members-changed', self._CHANNEL_6, True) + elif access_point.name.endswith('11'): + self._networks[self._CHANNEL_11] = access_point + self.emit('members-changed', self._CHANNEL_11, True) + + def is_sugar_adhoc_access_point(self, ap_object_path): + """Checks whether an access point is part of a sugar Ad-hoc network. + + Keyword arguments: + ap_object_path -- Access Point object path + + Return: Boolean + + """ + for access_point in self._networks.values(): + if access_point is not None: + if access_point.model.object_path == ap_object_path: + return True + return False + + def remove_access_point(self, ap_object_path): + """Remove an access point from a sugar Ad-hoc network. + + Keyword arguments: + ap_object_path -- Access Point object path + + """ + for channel in self._networks: + if self._networks[channel] is not None: + if self._networks[channel].model.object_path == ap_object_path: + self.emit('members-changed', channel, False) + self._networks[channel] = None + break diff --git a/src/jarabe/model/bundleregistry.py b/src/jarabe/model/bundleregistry.py index ac785fd..924c18f 100644 --- a/src/jarabe/model/bundleregistry.py +++ b/src/jarabe/model/bundleregistry.py @@ -20,6 +20,7 @@ import logging import traceback import sys +import gconf import gobject import gio import simplejson @@ -27,6 +28,7 @@ import simplejson from sugar.bundle.activitybundle import ActivityBundle from sugar.bundle.contentbundle import ContentBundle from jarabe.journal.journalentrybundle import JournalEntryBundle +from sugar.bundle.bundleversion import NormalizedVersion from sugar.bundle.bundle import MalformedBundleException, \ AlreadyInstalledException, RegistrationException from sugar import env @@ -62,6 +64,14 @@ class BundleRegistry(gobject.GObject): self._last_defaults_mtime = -1 self._favorite_bundles = {} + client = gconf.client_get_default() + self._protected_activities = client.get_list( + '/desktop/sugar/protected_activities', + gconf.VALUE_STRING) + + if self._protected_activities is None: + self._protected_activities = [] + try: self._load_favorites() except Exception: @@ -141,14 +151,16 @@ class BundleRegistry(gobject.GObject): return for bundle_id in default_activities: - max_version = -1 + max_version = '0' for bundle in self._bundles: if bundle.get_bundle_id() == bundle_id and \ - max_version < bundle.get_activity_version(): + NormalizedVersion(max_version) < \ + NormalizedVersion(bundle.get_activity_version()): max_version = bundle.get_activity_version() key = self._get_favorite_key(bundle_id, max_version) - if max_version > -1 and key not in self._favorite_bundles: + if NormalizedVersion(max_version) > NormalizedVersion('0') and \ + key not in self._favorite_bundles: self._favorite_bundles[key] = None logging.debug('After merging: %r' % self._favorite_bundles) @@ -272,6 +284,9 @@ class BundleRegistry(gobject.GObject): key = self._get_favorite_key(bundle_id, version) return key in self._favorite_bundles + def is_activity_protected(self, bundle_id): + return bundle_id in self._protected_activities + def set_bundle_position(self, bundle_id, version, x, y): key = self._get_favorite_key(bundle_id, version) if key not in self._favorite_bundles: @@ -324,8 +339,8 @@ class BundleRegistry(gobject.GObject): for installed_bundle in self._bundles: if bundle.get_bundle_id() == installed_bundle.get_bundle_id() and \ - bundle.get_activity_version() == \ - installed_bundle.get_activity_version(): + NormalizedVersion(bundle.get_activity_version()) == \ + NormalizedVersion(installed_bundle.get_activity_version()): return True return False @@ -338,15 +353,15 @@ class BundleRegistry(gobject.GObject): for installed_bundle in self._bundles: if bundle.get_bundle_id() == installed_bundle.get_bundle_id() and \ - bundle.get_activity_version() == \ - installed_bundle.get_activity_version(): + NormalizedVersion(bundle.get_activity_version()) <= \ + NormalizedVersion(installed_bundle.get_activity_version()): raise AlreadyInstalledException elif bundle.get_bundle_id() == installed_bundle.get_bundle_id(): self.uninstall(installed_bundle, force=True) install_dir = env.get_user_activities_path() if isinstance(bundle, JournalEntryBundle): - install_path = bundle.install(install_dir, uid) + install_path = bundle.install(uid) else: install_path = bundle.install(install_dir) @@ -371,7 +386,8 @@ class BundleRegistry(gobject.GObject): act = self.get_bundle(bundle.get_bundle_id()) if not force and \ - act.get_activity_version() != bundle.get_activity_version(): + NormalizedVersion(act.get_activity_version()) != \ + NormalizedVersion(bundle.get_activity_version()): logging.warning('Not uninstalling, different bundle present') return elif not act.get_path().startswith(env.get_user_activities_path()): diff --git a/src/jarabe/model/network.py b/src/jarabe/model/network.py index c1f7969..f0297c9 100644 --- a/src/jarabe/model/network.py +++ b/src/jarabe/model/network.py @@ -1,6 +1,7 @@ # Copyright (C) 2008 Red Hat, Inc. # Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer -# Copyright (C) 2009 One Laptop per Child +# Copyright (C) 2009-2010 One Laptop per Child +# Copyright (C) 2009 Paraguay Educa, Martin Abente # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,14 +22,20 @@ import os import time import dbus +import dbus.service import gobject import ConfigParser +import gconf +import ctypes from sugar import dispatch from sugar import env +from sugar.util import unique_id DEVICE_TYPE_802_3_ETHERNET = 1 DEVICE_TYPE_802_11_WIRELESS = 2 +DEVICE_TYPE_GSM_MODEM = 3 +DEVICE_TYPE_802_11_OLPC_MESH = 6 DEVICE_STATE_UNKNOWN = 0 DEVICE_STATE_UNMANAGED = 1 @@ -41,6 +48,9 @@ DEVICE_STATE_IP_CONFIG = 7 DEVICE_STATE_ACTIVATED = 8 DEVICE_STATE_FAILED = 9 +NM_CONNECTION_TYPE_802_11_WIRELESS = '802-11-wireless' +NM_CONNECTION_TYPE_GSM = 'gsm' + NM_ACTIVE_CONNECTION_STATE_UNKNOWN = 0 NM_ACTIVE_CONNECTION_STATE_ACTIVATING = 1 NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2 @@ -80,9 +90,48 @@ NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManagerSettings.Connection' NM_SECRETS_IFACE = 'org.freedesktop.NetworkManagerSettings.Connection.Secrets' NM_ACCESSPOINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' +GSM_USERNAME_PATH = '/desktop/sugar/network/gsm/username' +GSM_PASSWORD_PATH = '/desktop/sugar/network/gsm/password' +GSM_NUMBER_PATH = '/desktop/sugar/network/gsm/number' +GSM_APN_PATH = '/desktop/sugar/network/gsm/apn' +GSM_PIN_PATH = '/desktop/sugar/network/gsm/pin' +GSM_PUK_PATH = '/desktop/sugar/network/gsm/puk' + _nm_settings = None _conn_counter = 0 +def frequency_to_channel(frequency): + """Returns the channel matching a given radio channel frequency. If a + frequency is not in the dictionary channel 1 will be returned. + + Keyword arguments: + frequency -- The radio channel frequency in MHz. + + Return: Channel + + """ + ftoc = {2412: 1, 2417: 2, 2422: 3, 2427: 4, + 2432: 5, 2437: 6, 2442: 7, 2447: 8, + 2452: 9, 2457: 10, 2462: 11, 2467: 12, + 2472: 13} + if frequency not in ftoc: + logging.warning("The frequency %s can not be mapped to a channel, " \ + "defaulting to channel 1.", frequency) + return 1 + return ftoc[frequency] + +def is_sugar_adhoc_network(ssid): + """Checks whether an access point is a sugar Ad-hoc network. + + Keyword arguments: + ssid -- Ssid of the access point. + + Return: Boolean + + """ + return ssid.startswith('Ad-hoc Network') + + class WirelessSecurity(object): def __init__(self): self.key_mgmt = None @@ -103,11 +152,14 @@ class WirelessSecurity(object): return wireless_security class Wireless(object): + nm_name = "802-11-wireless" + def __init__(self): self.ssid = None self.security = None self.mode = None self.band = None + self.channel = None def get_dict(self): wireless = {'ssid': self.ssid} @@ -117,8 +169,27 @@ class Wireless(object): wireless['mode'] = self.mode if self.band: wireless['band'] = self.band + if self.channel: + wireless['channel'] = self.channel return wireless +class OlpcMesh(object): + nm_name = "802-11-olpc-mesh" + + def __init__(self, channel, anycast_addr): + self.channel = channel + self.anycast_addr = anycast_addr + + def get_dict(self): + ret = { + "ssid": dbus.ByteArray("olpc-mesh"), + "channel": self.channel, + } + + if self.anycast_addr: + ret["dhcp-anycast-address"] = dbus.ByteArray(self.anycast_addr) + return ret + class Connection(object): def __init__(self): self.id = None @@ -146,17 +217,60 @@ class IP4Config(object): ip4_config['method'] = self.method return ip4_config -class Settings(object): +class Serial(object): + def __init__(self): + self.baud = None + + def get_dict(self): + serial = {} + + if self.baud is not None: + serial['baud'] = self.baud + + return serial + +class Ppp(object): def __init__(self): + pass + + def get_dict(self): + ppp = {} + return ppp + +class Gsm(object): + def __init__(self): + self.apn = None + self.number = None + self.username = None + + def get_dict(self): + gsm = {} + + if self.apn is not None: + gsm['apn'] = self.apn + if self.number is not None: + gsm['number'] = self.number + if self.username is not None: + gsm['username'] = self.username + + return gsm + +class Settings(object): + def __init__(self, wireless_cfg=None): self.connection = Connection() self.wireless = Wireless() self.ip4_config = None self.wireless_security = None + if wireless_cfg is not None: + self.wireless = wireless_cfg + else: + self.wireless = Wireless() + def get_dict(self): settings = {} settings['connection'] = self.connection.get_dict() - settings['802-11-wireless'] = self.wireless.get_dict() + settings[self.wireless.nm_name] = self.wireless.get_dict() if self.wireless_security is not None: settings['802-11-wireless-security'] = \ self.wireless_security.get_dict() @@ -189,6 +303,41 @@ class Secrets(object): return settings +class SettingsGsm(object): + def __init__(self): + self.connection = Connection() + self.ip4_config = IP4Config() + self.serial = Serial() + self.ppp = Ppp() + self.gsm = Gsm() + + def get_dict(self): + settings = {} + + settings['connection'] = self.connection.get_dict() + settings['serial'] = self.serial.get_dict() + settings['ppp'] = self.ppp.get_dict() + settings['gsm'] = self.gsm.get_dict() + settings['ipv4'] = self.ip4_config.get_dict() + + return settings + +class SecretsGsm(object): + def __init__(self): + self.password = None + self.pin = None + self.puk = None + + def get_dict(self): + secrets = {} + if self.password is not None: + secrets['password'] = self.password + if self.pin is not None: + secrets['pin'] = self.pin + if self.puk is not None: + secrets['puk'] = self.puk + return {'gsm': secrets} + class NMSettings(dbus.service.Object): def __init__(self): bus = dbus.SystemBus() @@ -207,8 +356,8 @@ class NMSettings(dbus.service.Object): def NewConnection(self, connection_path): pass - def add_connection(self, ssid, conn): - self.connections[ssid] = conn + def add_connection(self, uuid, conn): + self.connections[uuid] = conn conn.secrets_request.connect(self.__secrets_request_cb) self.NewConnection(conn.path) @@ -216,6 +365,11 @@ class NMSettings(dbus.service.Object): self.secrets_request.send(self, connection=sender, response=kwargs['response']) + def clear_connections(self): + for connection in self.connections.values(): + connection.Removed() + self.connections = {} + class SecretsResponse(object): ''' Intermediate object to report the secrets from the dialog back to the connection object and which will inform NM @@ -244,10 +398,40 @@ class NMSettingsConnection(dbus.service.Object): self._settings = settings self._secrets = secrets + @dbus.service.signal(dbus_interface=NM_CONNECTION_IFACE, + signature='') + def Removed(self): + pass + + @dbus.service.signal(dbus_interface=NM_CONNECTION_IFACE, + signature='a{sa{sv}}') + def Updated(self, settings): + pass + def set_connected(self): - if not self._settings.connection.autoconnect: - self._settings.connection.autoconnect = True + if self._settings.connection.type == NM_CONNECTION_TYPE_GSM: + self._settings.connection.timestamp = int(time.time()) + elif not self._settings.connection.autoconnect: self._settings.connection.timestamp = int(time.time()) + self._settings.connection.autoconnect = True + self.Updated(self._settings.get_dict()) + self.save() + + try: + # try to flush resolver cache - SL#1940 + # ctypes' syntactic sugar does not work + # so we must get the func ptr explicitly + libc = ctypes.CDLL('libc.so.6') + res_init = getattr(libc, '__res_init') + res_init(None) + except: + logging.exception('Error calling libc.__res_init') + + def set_disconnected(self): + if self._settings.connection.autoconnect: + self._settings.connection.autoconnect = False + self._settings.connection.timestamp = None + self.Updated(self._settings.get_dict()) self.save() def set_secrets(self, secrets): @@ -258,6 +442,10 @@ class NMSettingsConnection(dbus.service.Object): return self._settings def save(self): + # We only save wifi settins + if self._settings.connection.type != NM_CONNECTION_TYPE_802_11_WIRELESS: + return + profile_path = env.get_profile_path() config_path = os.path.join(profile_path, 'nm', 'connections.cfg') @@ -336,7 +524,6 @@ class NMSettingsConnection(dbus.service.Object): else: reply(self._secrets.get_dict()) - class AccessPoint(gobject.GObject): __gsignals__ = { 'props-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, @@ -357,10 +544,10 @@ class AccessPoint(gobject.GObject): self.wpa_flags = 0 self.rsn_flags = 0 self.mode = 0 + self.channel = 0 def initialize(self): - model_props = dbus.Interface(self.model, - 'org.freedesktop.DBus.Properties') + model_props = dbus.Interface(self.model, dbus.PROPERTIES_IFACE) model_props.GetAll(NM_ACCESSPOINT_IFACE, byte_arrays=True, reply_handler=self._ap_properties_changed_cb, error_handler=self._get_all_props_error_cb) @@ -426,6 +613,8 @@ class AccessPoint(gobject.GObject): self.rsn_flags = properties['RsnFlags'] if 'Mode' in properties: self.mode = properties['Mode'] + if 'Frequency' in properties: + self.channel = frequency_to_channel(properties['Frequency']) self._initialized = True self.emit('props-changed', old_hash) @@ -452,36 +641,38 @@ def get_settings(): load_connections() return _nm_settings -def find_connection(ssid): +def find_connection_by_ssid(ssid): connections = get_settings().connections - if ssid in connections: - return connections[ssid] - else: - return None -def add_connection(ssid, settings, secrets=None): + for conn_index in connections: + connection = connections[conn_index] + if connection._settings.connection.type == NM_CONNECTION_TYPE_802_11_WIRELESS: + if connection._settings.wireless.ssid == ssid: + return connection + + return None + +def add_connection(uuid, settings, secrets=None): global _conn_counter path = NM_SETTINGS_PATH + '/' + str(_conn_counter) _conn_counter += 1 conn = NMSettingsConnection(path, settings, secrets) - _nm_settings.add_connection(ssid, conn) + _nm_settings.add_connection(uuid, conn) return conn -def load_connections(): +def load_wifi_connections(): profile_path = env.get_profile_path() config_path = os.path.join(profile_path, 'nm', 'connections.cfg') - config = ConfigParser.ConfigParser() - if not os.path.exists(config_path): if not os.path.exists(os.path.dirname(config_path)): os.makedirs(os.path.dirname(config_path), 0755) f = open(config_path, 'w') - config.write(f) f.close() + config = ConfigParser.ConfigParser() try: if not config.read(config_path): logging.error('Error reading the nm config file') @@ -534,4 +725,56 @@ def load_connections(): except ConfigParser.Error, e: logging.error('Error reading section: %s' % e) else: - add_connection(ssid, settings, secrets) + add_connection(uuid, settings, secrets) + +def count_connections(): + return len(get_settings().connections) + +def clear_connections(): + _nm_settings.clear_connections() + + profile_path = env.get_profile_path() + config_path = os.path.join(profile_path, 'nm', 'connections.cfg') + + if not os.path.exists(os.path.dirname(config_path)): + os.makedirs(os.path.dirname(config_path), 0755) + f = open(config_path, 'w') + f.close() + +def load_gsm_connection(): + client = gconf.client_get_default() + + settings = SettingsGsm() + settings.gsm.username = client.get_string(GSM_USERNAME_PATH) or '' + settings.gsm.number = client.get_string(GSM_NUMBER_PATH) or '' + settings.gsm.apn = client.get_string(GSM_APN_PATH) or '' + + secrets = SecretsGsm() + secrets.pin = client.get_string(GSM_PIN_PATH) or '' + secrets.puk = client.get_string(GSM_PUK_PATH) or '' + secrets.password = client.get_string(GSM_PASSWORD_PATH) or '' + + settings.connection.id = 'gsm' + settings.connection.type = NM_CONNECTION_TYPE_GSM + uuid = settings.connection.uuid = unique_id() + settings.connection.autoconnect = False + settings.ip4_config.method = 'auto' + settings.serial.baud = 115200 + + try: + add_connection(uuid, settings, secrets) + except Exception: + logging.exception('While adding gsm connection') + +def load_connections(): + load_wifi_connections() + load_gsm_connection() + +def find_gsm_connection(): + connections = get_settings().connections + + for connection in connections.values(): + if connection.get_settings().connection.type == NM_CONNECTION_TYPE_GSM: + return connection + + return None diff --git a/src/jarabe/model/olpcmesh.py b/src/jarabe/model/olpcmesh.py new file mode 100644 index 0000000..7873692 --- /dev/null +++ b/src/jarabe/model/olpcmesh.py @@ -0,0 +1,214 @@ +# Copyright (C) 2009, 2010 One Laptop per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +import gobject + +from jarabe.model import network +from jarabe.model.network import Settings +from jarabe.model.network import OlpcMesh as OlpcMeshSettings +from sugar.util import unique_id + +_NM_SERVICE = 'org.freedesktop.NetworkManager' +_NM_IFACE = 'org.freedesktop.NetworkManager' +_NM_PATH = '/org/freedesktop/NetworkManager' +_NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' +_NM_OLPC_MESH_IFACE = 'org.freedesktop.NetworkManager.Device.OlpcMesh' + +_XS_ANYCAST = "\xc0\x27\xc0\x27\xc0\x00" + +DEVICE_STATE_UNKNOWN = 0 +DEVICE_STATE_UNMANAGED = 1 +DEVICE_STATE_UNAVAILABLE = 2 +DEVICE_STATE_DISCONNECTED = 3 +DEVICE_STATE_PREPARE = 4 +DEVICE_STATE_CONFIG = 5 +DEVICE_STATE_NEED_AUTH = 6 +DEVICE_STATE_IP_CONFIG = 7 +DEVICE_STATE_ACTIVATED = 8 +DEVICE_STATE_FAILED = 9 + +class OlpcMeshManager(object): + def __init__(self, mesh_device): + self._bus = dbus.SystemBus() + + self.mesh_device = mesh_device + self.eth_device = self._get_companion_device() + + self._connection_queue = [] + """Stack of connections that we'll iterate through until we find one + that works. + + """ + + props = dbus.Interface(self.mesh_device, + 'org.freedesktop.DBus.Properties') + props.Get(_NM_DEVICE_IFACE, 'State', + reply_handler=self.__get_mesh_state_reply_cb, + error_handler=self.__get_state_error_cb) + + props = dbus.Interface(self.eth_device, + 'org.freedesktop.DBus.Properties') + props.Get(_NM_DEVICE_IFACE, 'State', + reply_handler=self.__get_eth_state_reply_cb, + error_handler=self.__get_state_error_cb) + + self._bus.add_signal_receiver(self.__eth_device_state_changed_cb, + signal_name='StateChanged', + path=self.eth_device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + + self._bus.add_signal_receiver(self.__mshdev_state_changed_cb, + signal_name='StateChanged', + path=self.mesh_device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + + self._idle_source = 0 + self._mesh_device_state = DEVICE_STATE_UNKNOWN + self._eth_device_state = DEVICE_STATE_UNKNOWN + + if self._have_configured_connections(): + self._start_automesh_timer() + else: + self._start_automesh() + + def _get_companion_device(self): + props = dbus.Interface(self.mesh_device, + 'org.freedesktop.DBus.Properties') + eth_device_o = props.Get(_NM_OLPC_MESH_IFACE, 'Companion') + return self._bus.get_object(_NM_SERVICE, eth_device_o) + + def _have_configured_connections(self): + return len(network.get_settings().connections) > 0 + + def _start_automesh_timer(self): + """Start our timer system which basically looks for 10 seconds of + inactivity on both devices, then starts automesh. + + """ + if self._idle_source != 0: + gobject.source_remove(self._idle_source) + self._idle_source = gobject.timeout_add_seconds(10, self._idle_check) + + def __get_state_error_cb(self, err): + logging.debug('Error getting the device state: %s', err) + + def __get_mesh_state_reply_cb(self, state): + self._mesh_device_state = state + self._maybe_schedule_idle_check() + + def __get_eth_state_reply_cb(self, state): + self._eth_device_state = state + self._maybe_schedule_idle_check() + + def __eth_device_state_changed_cb(self, new_state, old_state, reason): + """If a connection is activated on the eth device, stop trying our + automatic connections. + + """ + self._eth_device_state = new_state + self._maybe_schedule_idle_check() + + if new_state >= DEVICE_STATE_PREPARE \ + and new_state <= DEVICE_STATE_ACTIVATED \ + and len(self._connection_queue) > 0: + self._connection_queue = [] + + def __mshdev_state_changed_cb(self, new_state, old_state, reason): + self._mesh_device_state = new_state + self._maybe_schedule_idle_check() + + if new_state == DEVICE_STATE_FAILED: + self._try_next_connection_from_queue() + elif new_state == DEVICE_STATE_ACTIVATED \ + and len(self._connection_queue) > 0: + self._empty_connection_queue() + + def _maybe_schedule_idle_check(self): + if self._mesh_device_state == DEVICE_STATE_DISCONNECTED \ + and self._eth_device_state == DEVICE_STATE_DISCONNECTED: + self._start_automesh_timer() + + def _idle_check(self): + if self._mesh_device_state == DEVICE_STATE_DISCONNECTED \ + and self._eth_device_state == DEVICE_STATE_DISCONNECTED: + logging.debug("starting automesh due to inactivity") + self._start_automesh() + return False + + def _make_connection(self, channel, anycast_address=None): + wireless_config = OlpcMeshSettings(channel, anycast_address) + settings = Settings(wireless_cfg=wireless_config) + if not anycast_address: + settings.ip4_config = network.IP4Config() + settings.ip4_config.method = 'link-local' + settings.connection.id = 'olpc-mesh-' + str(channel) + settings.connection.uuid = unique_id() + settings.connection.type = '802-11-olpc-mesh' + connection = network.add_connection(settings.connection.id, settings) + return connection + + def __activate_reply_cb(self, connection): + logging.debug('Connection activated: %s', connection) + + def __activate_error_cb(self, err): + logging.error('Failed to activate connection: %s', err) + + def _activate_connection(self, channel, anycast_address=None): + logging.debug("activate channel %d anycast %r", + channel, anycast_address) + proxy = self._bus.get_object(_NM_SERVICE, _NM_PATH) + network_manager = dbus.Interface(proxy, _NM_IFACE) + connection = self._make_connection(channel, anycast_address) + + network_manager.ActivateConnection(network.SETTINGS_SERVICE, + connection.path, + self.mesh_device.object_path, + self.mesh_device.object_path, + reply_handler=self.__activate_reply_cb, + error_handler=self.__activate_error_cb) + + def _try_next_connection_from_queue(self): + if len(self._connection_queue) == 0: + return + + channel, anycast = self._connection_queue.pop() + self._activate_connection(channel, anycast) + + def _empty_connection_queue(self): + self._connection_queue = [] + + def user_activate_channel(self, channel): + """Activate a mesh connection on a user-specified channel. + Looks for XS first, then resorts to simple mesh.""" + self._empty_connection_queue() + self._connection_queue.append((channel, None)) + self._connection_queue.append((channel, _XS_ANYCAST)) + self._try_next_connection_from_queue() + + def _start_automesh(self): + """Start meshing automatically, intended when there are no better + networks to connect to. First looks for XS on all channels, then falls + back to simple mesh on channel 1.""" + self._empty_connection_queue() + self._connection_queue.append((1, None)) + self._connection_queue.append((11, _XS_ANYCAST)) + self._connection_queue.append((6, _XS_ANYCAST)) + self._connection_queue.append((1, _XS_ANYCAST)) + self._try_next_connection_from_queue() + diff --git a/src/jarabe/view/buddymenu.py b/src/jarabe/view/buddymenu.py index 35a8301..e9e9f8e 100644 --- a/src/jarabe/view/buddymenu.py +++ b/src/jarabe/view/buddymenu.py @@ -79,8 +79,8 @@ class BuddyMenu(Palette): self._update_invite_menu(activity) def _add_my_items(self): - item = MenuItem(_('My Settings'), 'preferences-system') - item.connect('activate', self.__controlpanel_activate_cb) + item = MenuItem(_('Shutdown'), 'system-shutdown') + item.connect('activate', self.__shutdown_activate_cb) self.menu.append(item) item.show() @@ -92,13 +92,8 @@ class BuddyMenu(Palette): self.menu.append(item) item.show() - item = MenuItem(_('Restart'), 'system-restart') - item.connect('activate', self.__reboot_activate_cb) - self.menu.append(item) - item.show() - - item = MenuItem(_('Shutdown'), 'system-shutdown') - item.connect('activate', self.__shutdown_activate_cb) + item = MenuItem(_('My Settings'), 'preferences-system') + item.connect('activate', self.__controlpanel_activate_cb) self.menu.append(item) item.show() diff --git a/src/jarabe/view/keyhandler.py b/src/jarabe/view/keyhandler.py index 1da1f6a..5358da8 100644 --- a/src/jarabe/view/keyhandler.py +++ b/src/jarabe/view/keyhandler.py @@ -47,6 +47,8 @@ _actions_table = { 'F2' : 'zoom_group', 'F3' : 'zoom_home', 'F4' : 'zoom_activity', + 'F5' : 'open_search', + 'F6' : 'frame', 'F9' : 'brightness_down', 'F10' : 'brightness_up', '<alt>F9' : 'brightness_min', @@ -249,9 +251,9 @@ class KeyHandler(object): # If either the xmodmap or xrandr command fails, check_call will fail # with CalledProcessError, which we raise. try: - subprocess.check_call(argv) subprocess.check_call(['xrandr', '-o', states[self._screen_rotation]]) + subprocess.check_call(argv) except OSError, e: if e.errno != errno.EINTR: raise diff --git a/src/jarabe/view/launcher.py b/src/jarabe/view/launcher.py index 6ddb04a..3071790 100644 --- a/src/jarabe/view/launcher.py +++ b/src/jarabe/view/launcher.py @@ -28,14 +28,20 @@ from sugar.graphics.xocolor import XoColor from jarabe.model import shell from jarabe.view.pulsingicon import CanvasPulsingIcon -class LaunchWindow(hippo.CanvasWindow): +class LaunchWindow(gtk.Window): def __init__(self, activity_id, icon_path, icon_color): - gobject.GObject.__init__( - self, type_hint=gtk.gdk.WINDOW_TYPE_HINT_NORMAL) + gobject.GObject.__init__(self) + + self.props.type_hint = gtk.gdk.WINDOW_TYPE_HINT_NORMAL + + canvas = hippo.Canvas() + canvas.modify_bg(gtk.STATE_NORMAL, style.COLOR_WHITE.get_gdk_color()) + self.add(canvas) + canvas.show() self._activity_id = activity_id self._box = LaunchBox(activity_id, icon_path, icon_color) - self.set_root(self._box) + canvas.set_root(self._box) self.connect('realize', self.__realize_cb) @@ -61,8 +67,7 @@ class LaunchWindow(hippo.CanvasWindow): class LaunchBox(hippo.CanvasBox): def __init__(self, activity_id, icon_path, icon_color): - gobject.GObject.__init__(self, orientation=hippo.ORIENTATION_VERTICAL, - background_color=style.COLOR_WHITE.get_int()) + gobject.GObject.__init__(self, orientation=hippo.ORIENTATION_VERTICAL) self._activity_id = activity_id self._activity_icon = CanvasPulsingIcon( diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py index b222fc7..170f42f 100644 --- a/src/jarabe/view/palettes.py +++ b/src/jarabe/view/palettes.py @@ -17,10 +17,9 @@ import os import statvfs from gettext import gettext as _ -import gconf import logging -import gobject +import gconf import gtk from sugar import env @@ -32,7 +31,6 @@ from sugar.graphics.xocolor import XoColor from sugar.activity import activityfactory from sugar.activity.activityhandle import ActivityHandle -from jarabe.model import bundleregistry from jarabe.model import shell from jarabe.view import launcher from jarabe.view.viewsource import setup_view_source @@ -107,12 +105,9 @@ class CurrentActivityPalette(BasePalette): class ActivityPalette(Palette): __gtype_name__ = 'SugarActivityPalette' - __gsignals__ = { - 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([])) - } - def __init__(self, activity_info): + self._activity_info = activity_info + client = gconf.client_get_default() color = XoColor(client.get_string("/desktop/sugar/user/color")) activity_icon = Icon(file=activity_info.get_icon(), @@ -122,14 +117,6 @@ class ActivityPalette(Palette): Palette.__init__(self, primary_text=activity_info.get_name(), icon=activity_icon) - registry = bundleregistry.get_registry() - - self._bundle = activity_info - self._bundle_id = activity_info.get_bundle_id() - self._version = activity_info.get_activity_version() - self._favorite = registry.is_bundle_favorite(self._bundle_id, - self._version) - xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(), style.COLOR_TRANSPARENT.get_svg())) menu_item = MenuItem(text_label=_('Start'), @@ -141,46 +128,6 @@ class ActivityPalette(Palette): # TODO: start-with - self._favorite_item = MenuItem('') - self._favorite_icon = Icon(icon_name='emblem-favorite', - icon_size=gtk.ICON_SIZE_MENU) - self._favorite_item.set_image(self._favorite_icon) - self._favorite_item.connect('activate', - self.__change_favorite_activate_cb) - self.menu.append(self._favorite_item) - self._favorite_item.show() - - menu_item = MenuItem(_('Erase'), 'list-remove') - menu_item.connect('activate', self.__erase_activate_cb) - self.menu.append(menu_item) - menu_item.show() - - if not os.access(self._bundle.get_path(), os.W_OK): - menu_item.props.sensitive = False - - registry = bundleregistry.get_registry() - self._activity_changed_sid = registry.connect('bundle_changed', - self.__activity_changed_cb) - self._update_favorite_item() - - self.connect('destroy', self.__destroy_cb) - - def __destroy_cb(self, palette): - self.disconnect(self._activity_changed_sid) - - def _update_favorite_item(self): - label = self._favorite_item.child - if self._favorite: - label.set_text(_('Remove favorite')) - xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(), - style.COLOR_TRANSPARENT.get_svg())) - else: - label.set_text(_('Make favorite')) - client = gconf.client_get_default() - xo_color = XoColor(client.get_string("/desktop/sugar/user/color")) - - self._favorite_icon.props.xo_color = xo_color - def __start_activate_cb(self, menu_item): self.popdown(immediate=True) @@ -189,28 +136,11 @@ class ActivityPalette(Palette): activity_id = activityfactory.create_activity_id() launcher.add_launcher(activity_id, - self._bundle.get_icon(), + self._activity_info.get_icon(), xo_color) handle = ActivityHandle(activity_id) - activityfactory.create(self._bundle, handle) - - def __change_favorite_activate_cb(self, menu_item): - registry = bundleregistry.get_registry() - registry.set_bundle_favorite(self._bundle_id, - self._version, - not self._favorite) - - def __activity_changed_cb(self, activity_registry, activity_info): - if activity_info.get_bundle_id() == self._bundle_id and \ - activity_info.get_activity_version() == self._version: - registry = bundleregistry.get_registry() - self._favorite = registry.is_bundle_favorite(self._bundle_id, - self._version) - self._update_favorite_item() - - def __erase_activate_cb(self, menu_item): - self.emit('erase-activated') + activityfactory.create(self._activity_info, handle) class JournalPalette(BasePalette): def __init__(self, home_activity): |