diff options
Diffstat (limited to 'extensions/cpsection')
-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 |
10 files changed, 886 insertions, 113 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() + |