diff options
Diffstat (limited to 'extensions')
-rw-r--r-- | extensions/cpsection/aboutcomputer/model.py | 129 | ||||
-rw-r--r-- | extensions/cpsection/aboutcomputer/view.py | 155 | ||||
-rw-r--r-- | extensions/cpsection/datetime/model.py | 47 | ||||
-rw-r--r-- | extensions/cpsection/datetime/view.py | 185 | ||||
-rw-r--r-- | extensions/cpsection/modemconfiguration/Makefile.am | 1 | ||||
-rw-r--r-- | extensions/cpsection/modemconfiguration/config.py | 25 | ||||
-rwxr-xr-x | extensions/cpsection/modemconfiguration/model.py | 187 | ||||
-rw-r--r-- | extensions/cpsection/modemconfiguration/view.py | 198 | ||||
-rw-r--r-- | extensions/cpsection/network/model.py | 17 | ||||
-rw-r--r-- | extensions/cpsection/network/view.py | 55 | ||||
-rw-r--r-- | extensions/deviceicon/Makefile.am | 1 | ||||
-rw-r--r-- | extensions/deviceicon/network.py | 222 | ||||
-rw-r--r-- | extensions/deviceicon/resources.py | 217 |
13 files changed, 1291 insertions, 148 deletions
diff --git a/extensions/cpsection/aboutcomputer/model.py b/extensions/cpsection/aboutcomputer/model.py index 86d2e3f..5d327ea 100644 --- a/extensions/cpsection/aboutcomputer/model.py +++ b/extensions/cpsection/aboutcomputer/model.py @@ -1,4 +1,5 @@ # Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2010 Plan Ceibal <comunidad@plan.ceibal.edu.uy> # # 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 @@ -18,9 +19,12 @@ import os import logging import re +import ConfigParser +import time import subprocess from gettext import gettext as _ import errno +from datetime import datetime import dbus @@ -39,6 +43,9 @@ _DMI_DIRECTORY = '/sys/class/dmi/id' _SN = 'serial-number' _MODEL = 'openprom/model' +_XO_1_0_LEASE_PATH = '/security/lease.sig' +_XO_1_5_LEASE_PATH = '/bootpart/boot/security/lease.sig' + _logger = logging.getLogger('ControlPanel - AboutComputer') _not_available = _('Not available') @@ -53,6 +60,31 @@ def print_aboutcomputer(): print get_aboutcomputer() +def _get_lease_path(): + if os.path.exists(_XO_1_0_LEASE_PATH): + return _XO_1_0_LEASE_PATH + elif os.path.exists(_XO_1_5_LEASE_PATH): + return _XO_1_5_LEASE_PATH + else: + return '' + + +def get_lease_days(): + lease_file = _read_file(_get_lease_path()) + if lease_file is None: + return _not_available + + encoded_date = str(str.split(lease_file)[3]) + expiry_date = datetime.strptime(encoded_date + , '%Y%m%dT%H%M%SZ') + current_date = datetime.today() + days_remaining = (expiry_date - current_date).days + + # TRANS: Do not translate %s + str_days_remaining = _('%s days remaining' % str(days_remaining)) + return str_days_remaining + + def get_serial_number(): serial_no = None if os.path.exists(os.path.join(_OFW_TREE, _SN)): @@ -72,7 +104,10 @@ def print_serial_number(): def get_build_number(): - build_no = _read_file('/boot/olpc_build') + if os.path.isfile('/boot/olpc_build'): + build_no = _read_file('/boot/olpc_build') + elif os.path.isfile('/bootpart/olpc_build'): + build_no = _read_file('/bootpart/olpc_build') if build_no is None: build_no = _read_file('/etc/redhat-release') @@ -97,6 +132,15 @@ def print_build_number(): print get_build_number() +def get_model_laptop(): + from ceibal import laptop + + model_laptop = laptops.get_model_laptop() + if model_laptop is None or not model_laptop: + model_laptop = _not_available + return model_laptop + + def _parse_firmware_number(firmware_no): if firmware_no is None: firmware_no = _not_available @@ -226,3 +270,86 @@ def get_license(): except IOError: license_text = _not_available return license_text + + +def get_last_updated_on_field(): + + # Get the number of UNIX seconds of the last update date. + last_update_unix_seconds = {} + try: + last_update_unix_seconds = int(os.stat('/var/lib/rpm/Packages').st_mtime) + except: + msg_str = _('Information not available.') + _logger.exception(msg_str) + return msg_str + + + NO_UPDATE_MESSAGE = _('No update yet!') + + + # Check once again that 'last_update_unix_seconds' is not empty. + # You never know ! + if not last_update_unix_seconds: + return NO_UPDATE_MESSAGE + + if int(last_update_unix_seconds) == 1194004800: + return NO_UPDATE_MESSAGE + + + # If we reached here, we have the last-update-time, but it's in + # timestamp format. + # Using python-subprocess-module (no shell involved), to convert + # it into readable date-format; the hack being used (after + # removing '-u' option) is the first one mentioned at : + # http://www.commandlinefu.com/commands/view/3719/convert-unix-timestamp-to-date + environment = os.environ.copy() + environment['PATH'] = '%s:/usr/sbin' % (environment['PATH'], ) + + last_update_readable_format = {} + try: + last_update_readable_format = \ + subprocess.Popen(['date', '-d', + '1970-01-01 + ' + + str(last_update_unix_seconds) + + ' seconds'], + stdout=subprocess.PIPE, + env=environment).stdout.readlines()[0] + except: + msg_str = _('Information not available.') + _logger.exception(msg_str) + return msg_str + + if not last_update_readable_format: + return _('Information not available.') + + # Everything should be fine (hopefully :-) ) + return last_update_readable_format + + +def get_sugar_version(): + return config.version + + +def get_plazo(): + from ceibal import env + path_plazo = env.get_security_root() + try: + plazo = _read_file(os.path.join(path_plazo, "blacklist")).split("\n")[0].strip() + plazo = time.strftime( "%d-%m-%Y",time.strptime(plazo,'%Y%m%d')) + except: + plazo = _not_available + + return plazo + +def get_act(): + from ceibal import env + path_act = env.get_updates_root() + parser = ConfigParser.ConfigParser() + salida = parser.read(os.path.join(path_act, "mi_version")) + if salida == []: + version = _not_available + else: + version = '' + for seccion in parser.sections(): + version = "%s%s: %s\n" %(version,seccion,parser.get(seccion,'version')) + return version diff --git a/extensions/cpsection/aboutcomputer/view.py b/extensions/cpsection/aboutcomputer/view.py index f44ca51..1e11301 100644 --- a/extensions/cpsection/aboutcomputer/view.py +++ b/extensions/cpsection/aboutcomputer/view.py @@ -23,7 +23,6 @@ from gi.repository import Gdk from sugar3.graphics import style -from jarabe import config from jarabe.controlpanel.sectionview import SectionView @@ -65,24 +64,51 @@ class AboutComputer(SectionView): vbox_identity.set_border_width(style.DEFAULT_SPACING * 2) vbox_identity.set_spacing(style.DEFAULT_SPACING) - box_identity = Gtk.HBox(spacing=style.DEFAULT_SPACING) - label_serial = Gtk.Label(label=_('Serial Number:')) - label_serial.set_alignment(1, 0) - label_serial.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - box_identity.pack_start(label_serial, False, True, 0) - self._group.add_widget(label_serial) - label_serial.show() - label_serial_no = Gtk.Label(label=self._model.get_serial_number()) - label_serial_no.set_alignment(0, 0) - box_identity.pack_start(label_serial_no, False, True, 0) - label_serial_no.show() - vbox_identity.pack_start(box_identity, False, True, 0) - box_identity.show() + self._setup_component_if_applicable(None, + _('Serial Number:'), + self._model.get_serial_number, + vbox_identity) + + self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_lease', + _('Lease:'), + self._model.get_lease_days, + vbox_identity) self._vbox.pack_start(vbox_identity, False, True, 0) vbox_identity.show() + def _is_feature_to_be_shown(slf, gconf_key): + if gconf_key is None: + return True + + from gi.repository import GConf + client = GConf.Client.get_default() + + return client.get_bool(gconf_key) is True + + def _setup_component_if_applicable(self, gconf_key, key, value_func, packer): + if not self._is_feature_to_be_shown(gconf_key): + return + + # Now that we do need to show, fetch the value. + print value_func + value = value_func() + + box = Gtk.HBox(spacing=style.DEFAULT_SPACING) + key_label = Gtk.Label(label=key) + key_label.set_alignment(1, 0) + key_label.modify_fg(Gtk.StateType.NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + box.pack_start(key_label, False, True, 0) + self._group.add_widget(key_label) + key_label.show() + value_label = Gtk.Label(label=value) + value_label.set_alignment(0, 0) + box.pack_start(value_label, False, True, 0) + value_label.show() + packer.pack_start(box, False, True, 0) + box.show() + def _setup_software(self): separator_software = Gtk.HSeparator() self._vbox.pack_start(separator_software, False, True, 0) @@ -96,66 +122,45 @@ class AboutComputer(SectionView): box_software.set_border_width(style.DEFAULT_SPACING * 2) box_software.set_spacing(style.DEFAULT_SPACING) - box_build = Gtk.HBox(spacing=style.DEFAULT_SPACING) - label_build = Gtk.Label(label=_('Build:')) - label_build.set_alignment(1, 0) - label_build.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - box_build.pack_start(label_build, False, True, 0) - self._group.add_widget(label_build) - label_build.show() - label_build_no = Gtk.Label(label=self._model.get_build_number()) - label_build_no.set_alignment(0, 0) - box_build.pack_start(label_build_no, False, True, 0) - label_build_no.show() - box_software.pack_start(box_build, False, True, 0) - box_build.show() - - box_sugar = Gtk.HBox(spacing=style.DEFAULT_SPACING) - label_sugar = Gtk.Label(label=_('Sugar:')) - label_sugar.set_alignment(1, 0) - label_sugar.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - box_sugar.pack_start(label_sugar, False, True, 0) - self._group.add_widget(label_sugar) - label_sugar.show() - label_sugar_ver = Gtk.Label(label=config.version) - label_sugar_ver.set_alignment(0, 0) - box_sugar.pack_start(label_sugar_ver, False, True, 0) - label_sugar_ver.show() - box_software.pack_start(box_sugar, False, True, 0) - box_sugar.show() - - box_firmware = Gtk.HBox(spacing=style.DEFAULT_SPACING) - label_firmware = Gtk.Label(label=_('Firmware:')) - label_firmware.set_alignment(1, 0) - label_firmware.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - box_firmware.pack_start(label_firmware, False, True, 0) - self._group.add_widget(label_firmware) - label_firmware.show() - label_firmware_no = Gtk.Label(label=self._model.get_firmware_number()) - label_firmware_no.set_alignment(0, 0) - box_firmware.pack_start(label_firmware_no, False, True, 0) - label_firmware_no.show() - box_software.pack_start(box_firmware, False, True, 0) - box_firmware.show() - - box_wireless_fw = Gtk.HBox(spacing=style.DEFAULT_SPACING) - label_wireless_fw = Gtk.Label(label=_('Wireless Firmware:')) - label_wireless_fw.set_alignment(1, 0) - label_wireless_fw.modify_fg(Gtk.StateType.NORMAL, - style.COLOR_SELECTION_GREY.get_gdk_color()) - box_wireless_fw.pack_start(label_wireless_fw, False, True, 0) - self._group.add_widget(label_wireless_fw) - label_wireless_fw.show() - wireless_fw_no = self._model.get_wireless_firmware() - label_wireless_fw_no = Gtk.Label(label=wireless_fw_no) - label_wireless_fw_no.set_alignment(0, 0) - box_wireless_fw.pack_start(label_wireless_fw_no, False, True, 0) - label_wireless_fw_no.show() - box_software.pack_start(box_wireless_fw, False, True, 0) - box_wireless_fw.show() + self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_model', + _('Model:'), + self._model.get_model_laptop, + box_software) + + self._setup_component_if_applicable(None, + _('Build:'), + self._model.get_build_number, + box_software) + + self._setup_component_if_applicable(None, + _('Sugar:'), + self._model.get_sugar_version, + box_software) + + self._setup_component_if_applicable(None, + _('Firmware:'), + self._model.get_firmware_number, + box_software) + + self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_wireless_firmware', + _('Wireless Firmware:'), + self._model.get_wireless_firmware, + box_software) + + self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_plazo', + _('Plazo:'), + self._model.get_plazo, + box_software) + + self._setup_component_if_applicable('/desktop/sugar/extensions/aboutcomputer/display_version_de_actual', + _('VersiĆ³n de ActualizaciĆ³n:'), + self._model.get_act, + box_software) + + self._setup_component_if_applicable(None, + _('Last Updated On:'), + self._model.get_last_updated_on_field, + box_software) self._vbox.pack_start(box_software, False, True, 0) box_software.show() diff --git a/extensions/cpsection/datetime/model.py b/extensions/cpsection/datetime/model.py index c9b4586..f73bef7 100644 --- a/extensions/cpsection/datetime/model.py +++ b/extensions/cpsection/datetime/model.py @@ -21,10 +21,57 @@ # import os +import logging + from gettext import gettext as _ from gi.repository import GConf _zone_tab = '/usr/share/zoneinfo/zone.tab' +NTPDATE_PATH = '/usr/sbin/ntpdate' +NTP_SERVER_CONFIG_FILENAME = '/etc/ntp/step-tickers' + +_logger = logging.getLogger('ControlPanel - TimeZone') + + +def is_ntp_servers_config_feature_available(): + return os.path.exists(NTPDATE_PATH) + + +def get_ntp_servers(): + servers = [] + + # If the file does not exist, return. + if not os.path.exists(NTP_SERVER_CONFIG_FILENAME): + return servers + + f = open(NTP_SERVER_CONFIG_FILENAME, 'r') + for server in f.readlines(): + servers.append(server.rstrip('\n')) + f.close() + + return servers + + +def set_ntp_servers(servers): + + # First remove the old ssid-file, if it exists. + if os.path.exists(NTP_SERVER_CONFIG_FILENAME): + try: + os.remove(NTP_SERVER_CONFIG_FILENAME) + except: + _logger.exception('Error removing file.') + return + + # Do nothing and return, if the values-list is empty + if len(servers) == 0: + return + + # If we reach here, we have a non-empty ssid-values-list. + f = open(NTP_SERVER_CONFIG_FILENAME, 'w') + for server in servers: + if len(server) > 0: + f.write(server + '\n') + f.close() def _initialize(): diff --git a/extensions/cpsection/datetime/view.py b/extensions/cpsection/datetime/view.py index 64789b4..4ad94ca 100644 --- a/extensions/cpsection/datetime/view.py +++ b/extensions/cpsection/datetime/view.py @@ -20,11 +20,130 @@ from gettext import gettext as _ from sugar3.graphics import style from sugar3.graphics import iconentry +from sugar3.graphics.icon import Icon from jarabe.controlpanel.sectionview import SectionView from jarabe.controlpanel.inlinealert import InlineAlert +class AddRemoveWidget(Gtk.HBox): + + def __init__(self, label, add_button_clicked_cb, + remove_button_clicked_cb, index): + Gtk.Box.__init__(self) + self.set_homogeneous(False) + self.set_spacing(10) + + self._index = index + self._add_button_added = False + self._remove_button_added = False + + self._entry_box = Gtk.Entry() + self._entry_box.set_text(label) + self._entry_box.set_width_chars(40) + self.pack_start(self._entry_box, False, False, 0) + self._entry_box.show() + + add_icon = Icon(icon_name='list-add') + self._add_button = Gtk.Button() + self._add_button.set_image(add_icon) + self._add_button.connect('clicked', + add_button_clicked_cb, + self) + + remove_icon = Icon(icon_name='list-remove') + self._remove_button = Gtk.Button() + self._remove_button.set_image(remove_icon) + self._remove_button.connect('clicked', + remove_button_clicked_cb, + self) + self.__add_add_button() + self.__add_remove_button() + + def _get_index(self): + return self._index + + def _set_index(self, value): + self._index = value + + def _get_entry(self): + return self._entry_box.get_text() + + def __add_add_button(self): + self.pack_start(self._add_button, False, False, 0) + self._add_button.show() + self._add_button_added = True + + def _remove_remove_button_if_not_already(self): + if self._remove_button_added: + self.__remove_remove_button() + + def __remove_remove_button(self): + self.remove(self._remove_button) + self._remove_button_added = False + + def _add_remove_button_if_not_already(self): + if not self._remove_button_added: + self.__add_remove_button() + + def __add_remove_button(self): + self.pack_start(self._remove_button, False, False, 0) + self._remove_button.show() + self._remove_button_added = True + + +class MultiWidget(Gtk.VBox): + + def __init__(self): + Gtk.VBox.__init__(self) + + def _add_widget(self, label): + new_widget = AddRemoveWidget(label, + self.__add_button_clicked_cb, + self.__remove_button_clicked_cb, + len(self.get_children())) + self.add(new_widget) + new_widget.show() + self.show() + self._update_remove_button_statuses() + + def __add_button_clicked_cb(self, add_button, + add_button_container): + self._add_widget('') + self._update_remove_button_statuses() + + def __remove_button_clicked_cb(self, remove_button, + remove_button_container): + for child in self.get_children(): + if child._get_index() > remove_button_container._get_index(): + child._set_index(child._get_index() - 1) + + self.remove(remove_button_container) + self._update_remove_button_statuses() + + def _update_remove_button_statuses(self): + children = self.get_children() + + # Now, if there is only one entry, remove-button + # should not be shown. + if len(children) == 1: + children[0]._remove_remove_button_if_not_already() + + # Alternatively, if there are more than 1 entries, + # remove-button should be shown for all. + if len(children) > 1: + for child in children: + child._add_remove_button_if_not_already() + + + def _get_entries(self): + entries = [] + for child in self.get_children(): + entries.append(child._get_entry()) + + return entries + + class TimeZone(SectionView): def __init__(self, model, alerts): SectionView.__init__(self) @@ -64,7 +183,10 @@ class TimeZone(SectionView): self._treeview.set_search_entry(self._entry) self._treeview.set_search_equal_func(self._search, None) self._treeview.set_search_column(0) - self._scrolled_window.add(self._treeview) + self._timezone_box = Gtk.VBox() + self._scrolled_window.add(self._timezone_box) + self._timezone_box.show_all() + self._timezone_box.add(self._treeview) self._treeview.show() self._timezone_column = Gtk.TreeViewColumn(_('Timezone')) @@ -74,19 +196,29 @@ class TimeZone(SectionView): self._timezone_column.set_sort_column_id(0) self._treeview.append_column(self._timezone_column) - self.pack_start(self._scrolled_window, True, True, 0) - self._scrolled_window.show() + self._container = Gtk.VBox() + self._container.set_homogeneous(False) + self._container.pack_start(self._scrolled_window, True, True, 0) + self._container.set_spacing(style.DEFAULT_SPACING) + self._container.show_all() + self.pack_start(self._container, True, True, 0) self._zone_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING) - self.pack_start(self._zone_alert_box, False, False, 0) - + self._timezone_box.pack_start(self._zone_alert_box, False, False, 0) self._zone_alert = InlineAlert() self._zone_alert_box.pack_start(self._zone_alert, True, True, 0) if 'zone' in self.restart_alerts: self._zone_alert.props.msg = self.restart_msg self._zone_alert.show() + + # Not showing this, as this hides the selected timezone. + # Instead, the alert will anyways be shown when user clicks + # on "Ok". + #self._zone_alert.show() self._zone_alert_box.show() + self._ntp_ui_setup = False + self.setup() def setup(self): @@ -102,6 +234,45 @@ class TimeZone(SectionView): self.needs_restart = False self._cursor_change_handler = self._treeview.connect( \ 'cursor-changed', self.__zone_changed_cd) + if self._model.is_ntp_servers_config_feature_available(): + self.setup_ui_for_ntp_server_config() + + def setup_ui_for_ntp_server_config(self): + if self._ntp_ui_setup: + return + self._ntp_ui_setup = True + + self._ntp_scrolled_window = Gtk.ScrolledWindow() + self._ntp_scrolled_window.set_policy(Gtk.PolicyType.NEVER, + Gtk.PolicyType.AUTOMATIC) + box_ntp_servers_config = Gtk.VBox() + box_ntp_servers_config.set_spacing(style.DEFAULT_SPACING) + + separator_ntp_servers_config= Gtk.HSeparator() + self._container.pack_start(separator_ntp_servers_config, + False, False, 0) + separator_ntp_servers_config.show() + + label_ntp_servers_config = Gtk.Label(_('NTP Servers Configuration')) + label_ntp_servers_config.set_alignment(0, 0) + self._container.pack_start(label_ntp_servers_config, + False, False, 0) + label_ntp_servers_config.show() + + self._widget_table = MultiWidget() + box_ntp_servers_config.pack_start(self._widget_table, False, False, 0) + box_ntp_servers_config.show_all() + + self._ntp_scrolled_window.add_with_viewport(box_ntp_servers_config) + self._container.pack_start(self._ntp_scrolled_window, True, True, 0) + self._ntp_scrolled_window.show_all() + + ntp_servers = self._model.get_ntp_servers() + if len(ntp_servers) == 0: + self._widget_table._add_widget('') + else: + for server in ntp_servers: + self._widget_table._add_widget(server) def undo(self): self._treeview.disconnect(self._cursor_change_handler) @@ -136,5 +307,7 @@ class TimeZone(SectionView): self.restart_alerts.append('zone') self.needs_restart = True self._zone_alert.props.msg = self.restart_msg - self._zone_alert.show() return False + + def perform_accept_actions(self): + self._model.set_ntp_servers(self._widget_table._get_entries()) diff --git a/extensions/cpsection/modemconfiguration/Makefile.am b/extensions/cpsection/modemconfiguration/Makefile.am index 3e2613e..46f8e70 100644 --- a/extensions/cpsection/modemconfiguration/Makefile.am +++ b/extensions/cpsection/modemconfiguration/Makefile.am @@ -3,4 +3,5 @@ sugardir = $(pkgdatadir)/extensions/cpsection/modemconfiguration sugar_PYTHON = \ __init__.py \ model.py \ + config.py \ view.py diff --git a/extensions/cpsection/modemconfiguration/config.py b/extensions/cpsection/modemconfiguration/config.py new file mode 100644 index 0000000..9e27814 --- /dev/null +++ b/extensions/cpsection/modemconfiguration/config.py @@ -0,0 +1,25 @@ +# -*- encoding: utf-8 -*- +# Copyright (C) 2010 Andres Ambrois +# +# 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 US + + +PROVIDERS_PATH = "/usr/share/mobile-broadband-provider-info/serviceproviders.xml" +PROVIDERS_FORMAT_SUPPORTED = "2.0" +COUNTRY_CODES_PATH = "/usr/share/zoneinfo/iso3166.tab" + +GSM_COUNTRY_PATH = '/desktop/sugar/network/gsm/country' +GSM_PROVIDERS_PATH = '/desktop/sugar/network/gsm/providers' +GSM_PLAN_PATH = '/desktop/sugar/network/gsm/plan' diff --git a/extensions/cpsection/modemconfiguration/model.py b/extensions/cpsection/modemconfiguration/model.py index f457293..e33d881 100755 --- a/extensions/cpsection/modemconfiguration/model.py +++ b/extensions/cpsection/modemconfiguration/model.py @@ -19,14 +19,27 @@ import logging import dbus from gi.repository import Gtk +import os +import locale +import logging +import gconf + +from xml.etree.cElementTree import ElementTree +from gettext import gettext as _ + from jarabe.model import network +from cpsection.modemconfiguration.config import PROVIDERS_PATH, \ + PROVIDERS_FORMAT_SUPPORTED, \ + COUNTRY_CODES_PATH + + def get_connection(): return network.find_gsm_connection() -def get_modem_settings(): +def get_modem_settings(callback): modem_settings = {} connection = get_connection() if not connection: @@ -48,6 +61,10 @@ def get_modem_settings(): modem_settings['password'] = gsm_secrets.get('password', '') modem_settings['pin'] = gsm_secrets.get('pin', '') + # sl#3800: We return the settings, via the "_secrets_cb() + # method", instead of busy-waiting. + callback(modem_settings) + def _secrets_err_cb(err): secrets_call_done[0] = True if isinstance(err, dbus.exceptions.DBusException) and \ @@ -57,14 +74,11 @@ def get_modem_settings(): logging.error('Error retrieving GSM secrets: %s', err) # must be called asynchronously as this re-enters the GTK main loop + # + # sl#3800: We return the settings, via the "_secrets_cb()" method, + # instead of busy-waiting. connection.get_secrets('gsm', _secrets_cb, _secrets_err_cb) - # wait til asynchronous execution completes - while not secrets_call_done[0]: - Gtk.main_iteration() - - return modem_settings - def _set_or_clear(_dict, key, value): """Update a dictionary value for a specific key. If value is None or @@ -98,3 +112,162 @@ def set_modem_settings(modem_settings): _set_or_clear(gsm_settings, 'apn', apn) _set_or_clear(gsm_settings, 'pin', pin) connection.update_settings(settings) + + +def has_providers_db(): + if not os.path.isfile(COUNTRY_CODES_PATH): + logging.debug("Mobile broadband provider database: Country " \ + "codes path %s not found.", COUNTRY_CODES_PATH) + return False + try: + tree = ElementTree(file=PROVIDERS_PATH) + except (IOError, SyntaxError), e: + logging.debug("Mobile broadband provider database: Could not read " \ + "provider information %s error=%s", PROVIDERS_PATH) + return False + else: + elem = tree.getroot() + if elem is None or elem.get('format') != PROVIDERS_FORMAT_SUPPORTED: + logging.debug("Mobile broadband provider database: Could not " \ + "read provider information. %s is wrong format.", + elem.get('format')) + return False + return True + + +class CountryListStore(Gtk.ListStore): + COUNTRY_CODE = locale.getdefaultlocale()[0][3:5].lower() + + def __init__(self): + Gtk.ListStore.__init__(self, str, object) + codes = {} + with open(COUNTRY_CODES_PATH) as codes_file: + for line in codes_file: + if line.startswith('#'): + continue + code, name = line.split('\t')[:2] + codes[code.lower()] = name.strip() + etree = ElementTree(file=PROVIDERS_PATH).getroot() + self._country_idx = None + i = 0 + + # This dictionary wil store the values, with "country-name" as + # the key, and "country-code" as the value. + temp_dict = {} + + for elem in etree.findall('.//country'): + code = elem.attrib['code'] + if code == self.COUNTRY_CODE: + self._country_idx = i + else: + i += 1 + if code in codes: + temp_dict[codes[code]] = elem + else: + temp_dict[code] = elem + + # Now, sort the list by country-names. + country_name_keys = temp_dict.keys() + country_name_keys.sort() + + for country_name in country_name_keys: + self.append((country_name, temp_dict[country_name])) + + def get_row_providers(self, row): + return self[row][1] + + def guess_country_row(self): + if self._country_idx is not None: + return self._country_idx + else: + return 0 + + def search_index_by_code(self, code): + for index in range(0, len(self)): + if self[index][0] == code: + return index + return -1 + + +class ProviderListStore(Gtk.ListStore): + def __init__(self, elem): + Gtk.ListStore.__init__(self, str, object) + for provider_elem in elem.findall('.//provider'): + apns = provider_elem.findall('.//apn') + if not apns: + # Skip carriers with CDMA entries only + continue + self.append((provider_elem.find('.//name').text, apns)) + + def get_row_plans(self, row): + return self[row][1] + + def guess_providers_row(self): + # Simply return the first entry as the default. + return 0 + + def search_index_by_code(self, code): + for index in range(0, len(self)): + if self[index][0] == code: + return index + return -1 + + +class PlanListStore(Gtk.ListStore): + LANG_NS_ATTR = '{http://www.w3.org/XML/1998/namespace}lang' + LANG = locale.getdefaultlocale()[0][:2] + DEFAULT_NUMBER = '*99#' + + def __init__(self, elems): + Gtk.ListStore.__init__(self, str, object) + for apn_elem in elems: + plan = {} + names = apn_elem.findall('.//name') + if names: + for name in names: + if name.get(self.LANG_NS_ATTR) is None: + # serviceproviders.xml default value + plan['name'] = name.text + elif name.get(self.LANG_NS_ATTR) == self.LANG: + # Great! We found a name value for our locale! + plan['name'] = name.text + break + else: + plan['name'] = _('Default') + plan['apn'] = apn_elem.get('value') + user = apn_elem.find('.//username') + if user is not None: + plan['username'] = user.text + else: + plan['username'] = '' + passwd = apn_elem.find('.//password') + if passwd is not None: + plan['password'] = passwd.text + else: + plan['password'] = '' + + plan['number'] = self.DEFAULT_NUMBER + + self.append((plan['name'], plan)) + + def get_row_plan(self, row): + return self[row][1] + + def guess_plan_row(self): + # Simply return the first entry as the default. + return 0 + + def search_index_by_code(self, code): + for index in range(0, len(self)): + if self[index][0] == code: + return index + return -1 + + +def get_gconf_setting_string(gconf_key): + client = gconf.client_get_default() + return client.get_string(gconf_key) or '' + +def set_gconf_setting_string(gconf_key, gconf_setting_string_value): + client = gconf.client_get_default() + client.set_string(gconf_key, gconf_setting_string_value) diff --git a/extensions/cpsection/modemconfiguration/view.py b/extensions/cpsection/modemconfiguration/view.py index d5aa399..d218330 100644 --- a/extensions/cpsection/modemconfiguration/view.py +++ b/extensions/cpsection/modemconfiguration/view.py @@ -24,6 +24,10 @@ from sugar3.graphics import style from jarabe.controlpanel.sectionview import SectionView +from cpsection.modemconfiguration.config import GSM_COUNTRY_PATH, \ + GSM_PROVIDERS_PATH, \ + GSM_PLAN_PATH + APPLY_TIMEOUT = 1000 @@ -64,6 +68,17 @@ class ModemConfiguration(SectionView): self.set_border_width(style.DEFAULT_SPACING) self.set_spacing(style.DEFAULT_SPACING) self._group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) + self._combo_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) + + scrolled_win = Gtk.ScrolledWindow() + scrolled_win.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + scrolled_win.show() + self.add(scrolled_win) + + main_box = Gtk.VBox(spacing=style.DEFAULT_SPACING) + main_box.set_border_width(style.DEFAULT_SPACING) + main_box.show() + scrolled_win.add_with_viewport(main_box) explanation = _('You will need to provide the following information' ' to set up a mobile broadband connection to a' @@ -71,41 +86,85 @@ class ModemConfiguration(SectionView): self._text = Gtk.Label(label=explanation) self._text.set_line_wrap(True) self._text.set_alignment(0, 0) - self.pack_start(self._text, False, False, 0) + main_box.pack_start(self._text, False, False, 0) self._text.show() + if model.has_providers_db(): + self._upper_box = Gtk.VBox(spacing=style.DEFAULT_SPACING) + self._upper_box.set_border_width(style.DEFAULT_SPACING) + main_box.pack_start(self._upper_box, True, True, 0) + self._upper_box.show() + + # Do not attach any 'change'-handlers for now. + # They will be attached (once per combobox), once the + # individual combobox is processed at startup. + self._country_store = model.CountryListStore() + self._country_combo = Gtk.ComboBox(model=self._country_store) + self._attach_combobox_widget(_('Country:'), + self._country_combo) + + self._providers_combo = Gtk.ComboBox() + self._attach_combobox_widget(_('Provider:'), + self._providers_combo) + + self._plan_combo = Gtk.ComboBox() + self._attach_combobox_widget(_('Plan:'), + self._plan_combo) + + separator = Gtk.HSeparator() + main_box.pack_start(separator, True, True, 0) + separator.show() + + self._lower_box = Gtk.VBox(spacing=style.DEFAULT_SPACING) + self._lower_box.set_border_width(style.DEFAULT_SPACING) + main_box.pack_start(self._lower_box, True, True, 0) + self._lower_box.show() + self._username_entry = EntryWithLabel(_('Username:')) - self._username_entry.entry.connect('changed', self.__entry_changed_cb) - self._group.add_widget(self._username_entry.label) - self.pack_start(self._username_entry, False, True, 0) - self._username_entry.show() + self._attach_entry_widget(self._username_entry) self._password_entry = EntryWithLabel(_('Password:')) - self._password_entry.entry.connect('changed', self.__entry_changed_cb) - self._group.add_widget(self._password_entry.label) - self.pack_start(self._password_entry, False, True, 0) - self._password_entry.show() + self._attach_entry_widget(self._password_entry) self._number_entry = EntryWithLabel(_('Number:')) - self._number_entry.entry.connect('changed', self.__entry_changed_cb) - self._group.add_widget(self._number_entry.label) - self.pack_start(self._number_entry, False, True, 0) - self._number_entry.show() + self._attach_entry_widget(self._number_entry) self._apn_entry = EntryWithLabel(_('Access Point Name (APN):')) - self._apn_entry.entry.connect('changed', self.__entry_changed_cb) - self._group.add_widget(self._apn_entry.label) - self.pack_start(self._apn_entry, False, True, 0) - self._apn_entry.show() + self._attach_entry_widget(self._apn_entry) self._pin_entry = EntryWithLabel(_('Personal Identity Number (PIN):')) - self._pin_entry.entry.connect('changed', self.__entry_changed_cb) - self._group.add_widget(self._pin_entry.label) - self.pack_start(self._pin_entry, False, True, 0) - self._pin_entry.show() + self._attach_entry_widget(self._pin_entry) self.setup() + def _attach_combobox_widget(self, label_text, combobox_obj): + box = Gtk.HBox(spacing=style.DEFAULT_SPACING) + label = Gtk.Label(label_text) + self._group.add_widget(label) + label.set_alignment(1, 0.5) + box.pack_start(label, False, False, 0) + label.show() + + self._combo_group.add_widget(combobox_obj) + cell = Gtk.CellRendererText() + cell.props.xalign = 0.5 + + cell.set_property('width-chars', 30) + + combobox_obj.pack_start(cell, True) + combobox_obj.add_attribute(cell, 'text', 0) + + box.pack_start(combobox_obj, False, False, 0) + combobox_obj.show() + self._upper_box.pack_start(box, False, False, 0) + box.show() + + def _attach_entry_widget(self, entry_with_label_obj): + entry_with_label_obj.entry.connect('changed', self.__entry_changed_cb) + self._group.add_widget(entry_with_label_obj.label) + self._lower_box.pack_start(entry_with_label_obj, True, True, 0) + entry_with_label_obj.show() + def undo(self): self._model.undo() @@ -113,12 +172,31 @@ class ModemConfiguration(SectionView): """Populate an entry with text, without triggering its 'changed' handler.""" entry = entrywithlabel.entry - entry.handler_block_by_func(self.__entry_changed_cb) + + # Do not block/unblock the callback functions. + # + # Thus, the savings will be persisted to the NM settings, + # whenever any setting on the UI changes (by user-intervention, + # or otherwise). + #entry.handler_block_by_func(self.__entry_changed_cb) entry.set_text(text) - entry.handler_unblock_by_func(self.__entry_changed_cb) + #entry.handler_unblock_by_func(self.__entry_changed_cb) def setup(self): - settings = self._model.get_modem_settings() + if self._model.has_providers_db(): + persisted_country = self._model.get_gconf_setting_string(GSM_COUNTRY_PATH) + if (self._model.has_providers_db()) and (persisted_country != ''): + self._country_combo.set_active(self._country_store.search_index_by_code(persisted_country)) + else: + self._country_combo.set_active(self._country_store.guess_country_row()) + + # Call the selected callback anyway, so as to chain-set the + # default values for providers and the plans. + self.__country_selected_cb(self._country_combo, setup=True) + + self._model.get_modem_settings(self.populate_entries) + + def populate_entries(self, settings): self._populate_entry(self._username_entry, settings.get('username', '')) self._populate_entry(self._number_entry, settings.get('number', '')) @@ -133,6 +211,78 @@ class ModemConfiguration(SectionView): self._timeout_sid = GObject.timeout_add(APPLY_TIMEOUT, self.__timeout_cb) + def _get_selected_text(self, combo): + active_iter = combo.get_active_iter() + return combo.get_model().get(active_iter, 0)[0] + + def __country_selected_cb(self, combo, setup=False): + country = self._get_selected_text(combo) + self._model.set_gconf_setting_string(GSM_COUNTRY_PATH, country) + + model = combo.get_model() + providers = model.get_row_providers(combo.get_active()) + self._providers_liststore = self._model.ProviderListStore(providers) + self._providers_combo.set_model(self._providers_liststore) + + # Set the default provider as well. + if setup: + persisted_provider = self._model.get_gconf_setting_string(GSM_PROVIDERS_PATH) + if persisted_provider == '': + self._providers_combo.set_active(self._providers_liststore.guess_providers_row()) + else: + self._providers_combo.set_active(self._providers_liststore.search_index_by_code(persisted_provider)) + else: + self._providers_combo.set_active(self._providers_liststore.guess_providers_row()) + + # Country-combobox processed once at startip; now, attach the + # change-handler. + self._country_combo.connect('changed', self.__country_selected_cb, False) + + # Call the callback, so that default provider may be set. + self.__provider_selected_cb(self._providers_combo, setup) + + def __provider_selected_cb(self, combo, setup=False): + provider = self._get_selected_text(combo) + self._model.set_gconf_setting_string(GSM_PROVIDERS_PATH, provider) + + model = combo.get_model() + plans = model.get_row_plans(combo.get_active()) + self._plan_liststore = self._model.PlanListStore(plans) + self._plan_combo.set_model(self._plan_liststore) + + # Set the default plan as well. + if setup: + persisted_plan = self._model.get_gconf_setting_string(GSM_PLAN_PATH) + if persisted_plan == '': + self._plan_combo.set_active(self._plan_liststore.guess_plan_row()) + else: + self._plan_combo.set_active(self._plan_liststore.search_index_by_code(persisted_plan)) + else: + self._plan_combo.set_active(self._plan_liststore.guess_plan_row()) + + # Providers-combobox processed once at startip; now, attach the + # change-handler. + self._providers_combo.connect('changed', self.__provider_selected_cb, False) + + # Call the callback, so that the default plan is set. + self.__plan_selected_cb(self._plan_combo, setup) + + def __plan_selected_cb(self, combo, setup=False): + plan = self._get_selected_text(combo) + self._model.set_gconf_setting_string(GSM_PLAN_PATH, plan) + + # Plan-combobox processed once at startip; now, attach the + # change-handler. + self._plan_combo.connect('changed', self.__plan_selected_cb, False) + + model = combo.get_model() + plan = model.get_row_plan(combo.get_active()) + + self._populate_entry(self._username_entry, plan['username']) + self._populate_entry(self._password_entry, plan['password']) + self._populate_entry(self._apn_entry, plan['apn']) + self._populate_entry(self._number_entry, plan['number']) + def __timeout_cb(self): self._timeout_sid = 0 settings = {} diff --git a/extensions/cpsection/network/model.py b/extensions/cpsection/network/model.py index ae9e64d..83c3cf1 100644 --- a/extensions/cpsection/network/model.py +++ b/extensions/cpsection/network/model.py @@ -18,6 +18,10 @@ import logging import dbus +import os +import subprocess +import logging + from gettext import gettext as _ from gi.repository import GConf @@ -30,6 +34,8 @@ _NM_IFACE = 'org.freedesktop.NetworkManager' KEYWORDS = ['network', 'jabber', 'radio', 'server'] +_logger = logging.getLogger('ControlPanel - Network') + class ReadError(Exception): def __init__(self, value): @@ -154,3 +160,14 @@ def set_publish_information(value): client = GConf.Client.get_default() client.set_bool('/desktop/sugar/collaboration/publish_gadget', value) return 0 + + +def launch_nm_connection_editor(): + environment = os.environ.copy() + environment['PATH'] = '%s:/usr/sbin' % (environment['PATH'], ) + + try: + subprocess.Popen(['-c', 'sudo nm-connection-editor --type=802-11-wireless'], + shell=True) + except: + _logger.exception('Error running nm-connection-editor') diff --git a/extensions/cpsection/network/view.py b/extensions/cpsection/network/view.py index 9b89375..e4332e4 100644 --- a/extensions/cpsection/network/view.py +++ b/extensions/cpsection/network/view.py @@ -17,6 +17,7 @@ from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GObject +from gi.repository import GConf from gettext import gettext as _ from sugar3.graphics import style @@ -30,6 +31,9 @@ ICON = 'module-network' TITLE = _('Network') _APPLY_TIMEOUT = 3000 +EXPLICIT_REBOOT_MESSAGE = _('Please restart your computer for changes to take effect.') + +gconf_client = GConf.Client.get_default() class Network(SectionView): @@ -51,6 +55,7 @@ class Network(SectionView): self._radio_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING) self._jabber_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING) + self._nm_connection_editor_alert_box = Gtk.HBox(spacing=style.DEFAULT_SPACING) scrolled = Gtk.ScrolledWindow() scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) @@ -178,8 +183,54 @@ class Network(SectionView): workspace.pack_start(box_mesh, False, True, 0) box_mesh.show() + if gconf_client.get_bool('/desktop/sugar/extensions/network/show_nm_connection_editor') is True: + box_nm_connection_editor = self.add_nm_connection_editor_launcher(workspace) + self.setup() + def add_nm_connection_editor_launcher(self, workspace): + separator_nm_connection_editor = Gtk.HSeparator() + workspace.pack_start(separator_nm_connection_editor, False, True, 0) + separator_nm_connection_editor.show() + + label_nm_connection_editor = Gtk.Label(_('Advanced Network Settings')) + label_nm_connection_editor.set_alignment(0, 0) + workspace.pack_start(label_nm_connection_editor, False, True, 0) + label_nm_connection_editor.show() + + box_nm_connection_editor = Gtk.VBox() + box_nm_connection_editor.set_border_width(style.DEFAULT_SPACING * 2) + box_nm_connection_editor.set_spacing(style.DEFAULT_SPACING) + + info = Gtk.Label(_("For more specific network settings, use " + "the NetworkManager Connection Editor.")) + + info.set_alignment(0, 0) + info.set_line_wrap(True) + box_nm_connection_editor.pack_start(info, False, True, 0) + + self._nm_connection_editor_alert = InlineAlert() + self._nm_connection_editor_alert.props.msg = EXPLICIT_REBOOT_MESSAGE + self._nm_connection_editor_alert_box.pack_start(self._nm_connection_editor_alert, + False, True, 0) + box_nm_connection_editor.pack_end(self._nm_connection_editor_alert_box, + False, True, 0) + self._nm_connection_editor_alert_box.show() + self._nm_connection_editor_alert.show() + + launch_button = Gtk.Button() + launch_button.set_alignment(0, 0) + launch_button.set_label(_('Launch')) + launch_button.connect('clicked', self.__launch_button_clicked_cb) + box_launch_button = Gtk.HBox() + box_launch_button.set_homogeneous(False) + box_launch_button.pack_start(launch_button, False, True, 0) + box_launch_button.show_all() + + box_nm_connection_editor.pack_start(box_launch_button, False, True, 0) + workspace.pack_start(box_nm_connection_editor, False, True, 0) + box_nm_connection_editor.show_all() + def setup(self): self._entry.set_text(self._model.get_jabber()) try: @@ -260,3 +311,7 @@ class Network(SectionView): self._model.clear_networks() if not self._model.have_networks(): self._clear_history_button.set_sensitive(False) + + def __launch_button_clicked_cb(self, launch_button): + self._model.launch_nm_connection_editor() + diff --git a/extensions/deviceicon/Makefile.am b/extensions/deviceicon/Makefile.am index 96a1753..42f3a4b 100644 --- a/extensions/deviceicon/Makefile.am +++ b/extensions/deviceicon/Makefile.am @@ -5,6 +5,7 @@ sugar_PYTHON = \ battery.py \ frame.py \ network.py \ + resources.py \ speaker.py \ speech.py \ touchpad.py \ diff --git a/extensions/deviceicon/network.py b/extensions/deviceicon/network.py index 79bc764..8ca9077 100644 --- a/extensions/deviceicon/network.py +++ b/extensions/deviceicon/network.py @@ -23,6 +23,8 @@ import logging import hashlib import socket import struct +import random +import re import datetime import time from gi.repository import Gtk @@ -30,6 +32,7 @@ import glib from gi.repository import GObject from gi.repository import GConf import dbus +import uuid from sugar3.graphics.icon import get_icon_state from sugar3.graphics import style @@ -55,6 +58,14 @@ _GSM_STATE_CONNECTING = 2 _GSM_STATE_CONNECTED = 3 _GSM_STATE_FAILED = 4 +_GSM_SHARING_PRIVATE = 0 +_GSM_SHARING_TRYING = 1 +_GSM_SHARING_NEIGHBORHOOD = 2 + +_GSM_SHARING_CHANNELS = [2,3,4,5,7,8,9,10,12,13] + +_wifi_device = None + class WirelessPalette(Palette): __gtype_name__ = 'SugarWirelessPalette' @@ -200,6 +211,8 @@ class GsmPalette(Palette): __gsignals__ = { 'gsm-connect': (GObject.SignalFlags.RUN_FIRST, None, ([])), 'gsm-disconnect': (GObject.SignalFlags.RUN_FIRST, None, ([])), + 'gsm-private': (GObject.SignalFlags.RUN_FIRST, None, ([])), + 'gsm-neighborhood': (GObject.SignalFlags.RUN_FIRST, None, ([])), } def __init__(self): @@ -208,6 +221,7 @@ class GsmPalette(Palette): self._current_state = None self._failed_connection = False + self._sharing_state = _GSM_SHARING_PRIVATE self.info_box = Gtk.VBox() @@ -216,6 +230,11 @@ class GsmPalette(Palette): self.info_box.pack_start(self._toggle_state_item, True, True, 0) self._toggle_state_item.show() + self._sharing_box = Gtk.VBox() + self.info_box.pack_start(self._sharing_box, True, True, 0) + self.__update_sharing_toggle_widget(_('Private (Click to share)'), 'zoom-home') + self._sharing_box.hide() + self.error_title_label = Gtk.Label(label="") self.error_title_label.set_alignment(0, 0.5) self.error_title_label.set_line_wrap(True) @@ -298,6 +317,9 @@ class GsmPalette(Palette): icon = Icon(icon_name='media-eject', \ icon_size=Gtk.IconSize.MENU) self._toggle_state_item.set_image(icon) + self.sharing_update_text() + self._sharing_toggle_item.show() + return elif self._current_state == _GSM_STATE_FAILED: message_error = self._get_error_by_nm_reason(reason) @@ -306,6 +328,8 @@ class GsmPalette(Palette): raise ValueError('Invalid GSM state while updating label and ' \ 'text, %s' % str(self._current_state)) + self._sharing_toggle_item.hide() + def __toggle_state_cb(self, menuitem): if self._current_state == _GSM_STATE_NOT_READY: pass @@ -366,6 +390,50 @@ class GsmPalette(Palette): message_tuple = (network.get_error_by_reason(reason), message) return message_tuple + def sharing_update_text(self): + if self._sharing_state == _GSM_SHARING_PRIVATE: + self.__update_sharing_toggle_widget(_('Private (Click to share)'), 'zoom-home') + + elif self._sharing_state == _GSM_SHARING_TRYING: + self.__update_sharing_toggle_widget(_('Please wait...'), 'zoom-home') + + elif self._sharing_state == _GSM_SHARING_NEIGHBORHOOD: + self.__update_sharing_toggle_widget(_('Neighborhood (Click to unshare)'), 'zoom-neighborhood') + + else: + raise ValueError('Invalid GSM sharing state while updating, %s' % \ + str(self._sharing_state)) + + def __update_sharing_toggle_widget(self, label, icon_name): + for child_widget in self._sharing_box.get_children(): + self._sharing_box.remove(child_widget) + + self._sharing_toggle_item = PaletteMenuItem('') + self._sharing_toggle_item.connect('activate', self.__sharing_toggle_cb) + self._sharing_toggle_item.set_label(label) + icon = Icon(icon_name=icon_name, icon_size=Gtk.IconSize.MENU) + self._sharing_toggle_item.set_image(icon) + icon.show() + self._sharing_box.pack_start(self._sharing_toggle_item, True, True, 0) + separator = PaletteMenuItemSeparator() + self._sharing_box.pack_start(separator, True, True, 0) + separator.show() + self._sharing_box.show_all() + + def __sharing_toggle_cb(self, menuitem): + if self._sharing_state == _GSM_SHARING_PRIVATE: + self.emit('gsm-neighborhood') + + elif self._sharing_state == _GSM_SHARING_TRYING: + pass + + elif self._sharing_state == _GSM_SHARING_NEIGHBORHOOD: + self.emit('gsm-private') + + else: + raise ValueError('Invalid GSM sharing state, %s' % \ + str(self._sharing_state)) + class WirelessDeviceView(ToolButton): @@ -527,17 +595,8 @@ class WirelessDeviceView(ToolButton): else: state = network.NM_DEVICE_STATE_UNKNOWN - if self._mode != network.NM_802_11_MODE_ADHOC and \ - network.is_sugar_adhoc_network(self._ssid) == False: - if state == network.NM_DEVICE_STATE_ACTIVATED: - icon_name = '%s-connected' % 'network-wireless' - else: - icon_name = 'network-wireless' - - icon_name = get_icon_state(icon_name, self._strength) - if icon_name: - self._icon.props.icon_name = icon_name - else: + if self._mode == network.NM_802_11_MODE_ADHOC and \ + network.is_sugar_adhoc_network(self._ssid): channel = network.frequency_to_channel(self._frequency) if state == network.NM_DEVICE_STATE_ACTIVATED: self._icon.props.icon_name = 'network-adhoc-%s-connected' \ @@ -729,8 +788,9 @@ class GsmDeviceView(TrayIcon): def __init__(self, device): self._connection_time_handler = None - self._connection_timestamp = 0 + self._shared_connection_path = None + self._target_dev_path = None client = GConf.Client.get_default() color = xocolor.XoColor(client.get_string('/desktop/sugar/user/color')) @@ -758,6 +818,8 @@ class GsmDeviceView(TrayIcon): palette.set_group_id('frame') palette.connect('gsm-connect', self.__gsm_connect_cb) palette.connect('gsm-disconnect', self.__gsm_disconnect_cb) + palette.connect('gsm-neighborhood', self.__gsm_start_sharing_cb) + palette.connect('gsm-private', self.__gsm_stop_sharing_cb) self._palette = palette @@ -775,42 +837,32 @@ class GsmDeviceView(TrayIcon): def __gsm_connect_cb(self, palette, data=None): connection = network.find_gsm_connection() if connection is not None: - connection.activate(self._device.object_path, - reply_handler=self.__connect_cb, - error_handler=self.__connect_error_cb) + network.activate_connection_by_path(connection.get_path(), + self._device.object_path, + reply_handler=self._connect_cb, + error_handler=self._connect_error_cb) else: self._palette.add_alert(_('No GSM connection available.'), \ _('Create a connection in the ' \ 'control panel.')) - def __connect_cb(self, active_connection): + def _connect_cb(self, active_connection_path): + self._base_gsm_connection_path = active_connection_path logging.debug('Connected successfully to gsm device, %s', - active_connection) + active_connection_path) - def __connect_error_cb(self, error): + def _connect_error_cb(self, error): raise RuntimeError('Error when connecting to gsm device, %s' % error) def __gsm_disconnect_cb(self, palette, data=None): - obj = self._bus.get_object(network.NM_SERVICE, network.NM_PATH) - netmgr = dbus.Interface(obj, network.NM_IFACE) - netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE) - active_connections_o = netmgr_props.Get(network.NM_IFACE, 'ActiveConnections') + network.get_manager().DeactivateConnection(self._base_gsm_connection_path, + reply_handler=self._disconnect_cb, + error_handler=self._disconnect_error_cb) - for conn_o in active_connections_o: - obj = self._bus.get_object(network.NM_IFACE, conn_o) - props = dbus.Interface(obj, dbus.PROPERTIES_IFACE) - devices = props.Get(network.NM_ACTIVE_CONN_IFACE, 'Devices') - if self._device.object_path in devices: - netmgr.DeactivateConnection( - conn_o, - reply_handler=self.__disconnect_cb, - error_handler=self.__disconnect_error_cb) - break - - def __disconnect_cb(self): + def _disconnect_cb(self): logging.debug('Disconnected successfully gsm device') - def __disconnect_error_cb(self, error): + def _disconnect_error_cb(self, error): raise RuntimeError('Error when disconnecting gsm device, %s' % error) def __state_changed_cb(self, new_state, old_state, reason): @@ -831,6 +883,10 @@ class GsmDeviceView(TrayIcon): gsm_state = _GSM_STATE_CONNECTED connection = network.find_gsm_connection() if connection is not None: + # Introspect the settings's keys once; else sometimes + # the key 'timestamp' gets missed. + connection.get_settings('connection').keys() + self._connection_timestamp = time.time() - \ connection.get_settings('connection')['timestamp'] self._connection_time_handler = GObject.timeout_add_seconds( \ @@ -879,6 +935,95 @@ class GsmDeviceView(TrayIcon): self._palette.update_connection_time(connection_time) return True + def __gsm_start_sharing_cb(self, palette): + if self._palette._sharing_state == _GSM_SHARING_PRIVATE: + logging.debug('GSM will start sharing now') + self._palette._sharing_state = _GSM_SHARING_TRYING + self._palette.sharing_update_text() + + self._target_device = _wifi_device + self._target_device_path = self._target_device.object_path + + client = GConf.Client.get_default() + nick = client.get_string('/desktop/sugar/user/nick') + nick = re.sub('\W', '', nick) + + name_format = '%s network' + format_length = len(name_format) - len('%s') + nick_length = 31 - format_length + name = name_format % nick[:nick_length] + + connection = network.find_connection_by_ssid(name) + if connection == None: + settings = network.Settings() + settings.connection.id = name + settings.connection.uuid = str(uuid.uuid4()) + settings.connection.type = '802-11-wireless' + settings.wireless.ssid = dbus.ByteArray(name) + settings.wireless.mode = 'adhoc' + settings.wireless.band = 'bg' + chosen_channel = random.randrange(len(_GSM_SHARING_CHANNELS)) + settings.wireless.channel = _GSM_SHARING_CHANNELS[chosen_channel] + settings.ip4_config = network.IP4Config() + settings.ip4_config.method = 'shared' + network.add_and_activate_connection(self._target_device, + settings, + '/', + self._gsm_sharing_ok_cb_for_add_and_activate, + self._gsm_sharing_error_cb) + else: + network.activate_connection_by_path(connection.get_path(), + self._target_device, + self._gsm_sharing_ok_cb, + self._gsm_sharing_error_cb) + + def _gsm_sharing_ok_cb_for_add_and_activate(self, + new_connection_path, + active_connection_path): + self._gsm_sharing_ok_cb(active_connection_path) + + def _gsm_sharing_ok_cb(self, connection_path): + logging.debug('GSM sharing is enabled') + self._shared_connection_path = connection_path + self._bus.add_signal_receiver(self._gsm_sharing_changed_cb, + signal_name='StateChanged', + path=self._target_device_path, + dbus_interface=network.NM_DEVICE_IFACE) + self._palette._sharing_state = _GSM_SHARING_NEIGHBORHOOD + self._palette.sharing_update_text() + + def _gsm_sharing_changed_cb(self, new_state, old_state, reason): + if new_state == network.NM_DEVICE_STATE_DISCONNECTED: + self._gsm_sharing_reset() + + def _gsm_sharing_reset(self): + logging.debug('GSM sharing is disabled') + if self._target_dev_path != None: + self._bus.remove_signal_receiver(self._gsm_sharing_changed_cb, + signal_name='StateChanged', + path=self._target_dev_path, + dbus_interface=network.NM_DEVICE_IFACE) + self._shared_connection_path = None + self._target_dev_path = None + self._palette._sharing_state = _GSM_SHARING_PRIVATE + self._palette.sharing_update_text() + + def _gsm_sharing_error_cb(self, error): + logging.debug('GSM sharing could not start: %s' % str(error)) + self._gsm_sharing_reset() + + def __gsm_stop_sharing_cb(self, palette): + logging.debug('GSM will stop sharing now') + network.get_manager().DeactivateConnection(self._shared_connection_path, + reply_handler=self._gsm_stop_sharing_ok_cb, + error_handler=self._gsm_stop_sharing_error_cb) + + def _gsm_stop_sharing_ok_cb(self): + self._gsm_sharing_reset() + + def _gsm_stop_sharing_error_cb(self): + logging.debug('GSM sharing could not stop') + class WirelessDeviceObserver(object): def __init__(self, device, tray): @@ -1056,6 +1201,8 @@ class NetworkManagerObserver(object): device = WiredDeviceObserver(nm_device, self._tray) self._devices[device_op] = device elif device_type == network.NM_DEVICE_TYPE_WIFI: + global _wifi_device + _wifi_device = nm_device device = WirelessDeviceObserver(nm_device, self._tray) self._devices[device_op] = device elif device_type == network.NM_DEVICE_TYPE_OLPC_MESH: @@ -1075,5 +1222,10 @@ class NetworkManagerObserver(object): del self._devices[device_op] +def get_wifi_device(): + global _wifi_device + return _wifi_device + + def setup(tray): device_observer = NetworkManagerObserver(tray) diff --git a/extensions/deviceicon/resources.py b/extensions/deviceicon/resources.py new file mode 100644 index 0000000..ef4adac --- /dev/null +++ b/extensions/deviceicon/resources.py @@ -0,0 +1,217 @@ +# Copyright (C) Anish Mangal <anishmangal2002@gmail.com> +# +# 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 + +from gettext import gettext as _ +import logging +import os + +from gi.repository import GObject +from gi.repository import Gtk +from gi.repository import GConf + +from sugar3.graphics.tray import TrayIcon +from sugar3.graphics.xocolor import XoColor +from sugar3.graphics.palette import Palette +from sugar3.graphics import style + + +_SYSTEM_MOODS = ['-sad', '-normal', '-happy'] +_ICON_NAME = 'computer' +_UPDATE_INTERVAL = 5 * 1000 + + +class DeviceView(TrayIcon): + + FRAME_POSITION_RELATIVE = 500 + + def __init__(self): + client = GConf.Client.get_default() + self._color = XoColor(client.get_string('/desktop/sugar/user/color')) + TrayIcon.__init__(self, icon_name=_ICON_NAME, xo_color=self._color) + self.create_palette() + self._icon_widget.connect('button-release-event', self._click_cb) + + def create_palette(self): + self.palette = ResourcePalette(_('System resources')) + self.palette.set_group_id('frame') + self.palette.add_timer() + self.palette.connect('system-mood-changed', + self._system_mood_changed_cb) + return self.palette + + def _system_mood_changed_cb(self, palette_, mood): + self.icon.props.icon_name = _ICON_NAME + mood + + def _click_cb(self, widget, event): + self.palette_invoker.notify_right_click() + + +class ResourcePalette(Palette): + __gsignals__ = { + 'system-mood-changed': (GObject.SignalFlags.RUN_FIRST, None, ([str])), + } + + def __init__(self, primary_text): + Palette.__init__(self, label=primary_text) + + self.vbox = Gtk.VBox() + self.set_content(self.vbox) + + self._cpu_text = Gtk.Label() + self.vbox.pack_start(self._cpu_text, True, True, 0) + self._cpu_bar = Gtk.ProgressBar() + self._cpu_bar.set_size_request( + style.zoom(style.GRID_CELL_SIZE * 4), -1) + self.vbox.pack_start(self._cpu_bar, True, True, 0) + + self._memory_text = Gtk.Label() + self.vbox.pack_start(self._memory_text, True, True, 0) + self._memory_bar = Gtk.ProgressBar() + self._memory_bar.set_size_request( + style.zoom(style.GRID_CELL_SIZE * 4), -1) + self.vbox.pack_start(self._memory_bar, True, True, 0) + + self._system_mood = None + try: + self._cpu_times = self._get_cpu_times_list() + except IOError: + logging.exception('An error ocurred while attempting to ' + 'read /proc/stat') + self._stop_computing_statistics() + + self.vbox.show() + self._cpu_text.show() + self._cpu_bar.show() + self._memory_text.show() + self._memory_bar.show() + + def add_timer(self): + GObject.timeout_add(_UPDATE_INTERVAL, self.__timer_cb) + + def _get_cpu_times_list(self): + """Return various cpu times as read from /proc/stat + + This method returns the following cpu times measured + in jiffies (1/100 of a second for x86 systems) + as an ordered list of numbers - [user, nice, + system, idle, iowait] where, + + user: normal processes executing in user mode + nice: niced processes executing in user mode + system: processes executing in kernel mode + idle: twiddling thumbs + iowait: waiting for I/O to complete + + Note: For systems having 2 or more CPU's, the above + numbers would be the cumulative sum of these times + for all CPU's present in the system. + + """ + return [int(count) + for count in file('/proc/stat').readline().split()[1:6]] + + def _percentage_cpu_available(self): + """ + Return free CPU resources as a percentage + + """ + _cpu_times_new = self._get_cpu_times_list() + _cpu_times_current = [(new - old) + for new, old in zip(_cpu_times_new, self._cpu_times)] + user_, nice_, system_, idle, iowait = _cpu_times_current + cpu_free = (idle + iowait) * 100.0 / sum(_cpu_times_current) + self._cpu_times = self._get_cpu_times_list() + return cpu_free + + def _percentage_memory_available(self): + """ + Return free memory as a percentage + """ + + for line in file('/proc/meminfo'): + name, value, unit_ = line.split()[:3] + if 'MemTotal:' == name: + total = int(value) + elif 'MemFree:' == name: + free = int(value) + elif 'Buffers:' == name: + buffers = int(value) + elif 'Cached:' == name: + cached = int(value) + elif 'Active:' == name: + break + return (free + buffers + cached) * 100.0 / total + + def __timer_cb(self): + try: + cpu_in_use = 100 - self._percentage_cpu_available() + memory_in_use = 100 - self._percentage_memory_available() + except IOError: + logging.exception('An error ocurred while trying to ' + 'retrieve resource usage statistics') + self._stop_and_show_error() + return False + else: + self._cpu_text.set_label(_('CPU in use: %d%%' % cpu_in_use)) + self._cpu_bar.set_fraction(float(cpu_in_use) / 100) + self._memory_text.set_label(_('Memory in use: %d%%' % + memory_in_use)) + self._memory_bar.set_fraction(float(memory_in_use) / 100) + + # both cpu_free and memory_free lie between 0-100 + system_mood = _SYSTEM_MOODS[ + int(300 - (cpu_in_use + 2 * memory_in_use)) // 100] + + # check if self._system_mood exists + try: + if self._system_mood != system_mood: + self.emit('system-mood-changed', system_mood) + self._system_mood = system_mood + except AttributeError: + self.emit('system-mood-changed', system_mood) + self._system_mood = system_mood + + return True + + def _stop_and_show_error(self): + """ + Stop computing usage statistics and display an error message + since we've hit an exception. + + """ + # Use the existing _cpu_text label to display the error. Remove + # everything else. + self._cpu_text.set_size_request( + style.zoom(style.GRID_CELL_SIZE * 4), -1) + self._cpu_text.set_line_wrap(True) + self._cpu_text.set_text(_('Cannot compute CPU and memory usage ' + 'statistics!')) + self.vbox.remove(self._cpu_bar) + self.vbox.remove(self._memory_text) + self.vbox.remove(self._memory_bar) + self.emit('system-mood-changed', '-error') + + +def setup(tray): + client = GConf.Client.get_default() + if not client.get_bool('/desktop/sugar/frame/show_network_resources'): + return + + if not (os.path.exists('/proc/stat') and os.path.exists('/proc/meminfo')): + logging.warning('Either /proc/stat or /proc/meminfo not present. Not ' + 'adding the CPU and memory usage icon to the frame') + return + tray.add_device(DeviceView()) |