[Sugar-devel] [sugar 0.98 PATCH] sl#1630, sl#3325: Support for mobile broadband provider database, plus ensure that all settings are persisted.

Ajay Garg ajay at activitycentral.com
Fri Jul 20 07:32:56 EDT 2012


First of all, please note that the first, major part of adding the database support 
has been done by ::

              Andres Ambrois <andresambrois at gmail.com>

Details of Andres's work are present at http://bugs.sugarlabs.org/ticket/1630


===============================================================================


So, this patch ::

   a)
   Ports Andres's patch from Sugar-0.88 to Sugar-0.98.

   b)
   Adds the functionality, to keep the "Country, Provider, Plan" settings
   persisted as well.

   c)
   Also, some refactoring has been done, to avoid duplicate code as much as possible.


================================================================================


Other relevant literature/discussions/logs at ::

   a) Wiki Feature Page ::
   http://wiki.sugarlabs.org/go/Features/3G_Support/Database_Support

   b)
   Feature Design ML ::
   http://lists.sugarlabs.org/archive/sugar-devel/2012-July/038429.html

   c)
   Brain-storming / Go-ahead meeting logs ::
   http://meeting.sugarlabs.org/sugar-meeting/meetings/2012-07-18T15:00:17


================================================================================


This patch has been tested to work on NM 0.9.

The 3G modem gets connected, only and only when the correct entries are entered 
(by choosing the correct plan, amongst the vast number of available plans).


================================================================================








 .../cpsection/modemconfiguration/Makefile.am       |    2 +
 extensions/cpsection/modemconfiguration/config.py  |   25 +++
 extensions/cpsection/modemconfiguration/model.py   |  163 ++++++++++++++++
 extensions/cpsection/modemconfiguration/view.py    |  198 +++++++++++++++++---
 4 files changed, 365 insertions(+), 23 deletions(-)
 create mode 100644 extensions/cpsection/modemconfiguration/config.py

diff --git a/extensions/cpsection/modemconfiguration/Makefile.am b/extensions/cpsection/modemconfiguration/Makefile.am
index 3e2613e..525e02e 100644
--- a/extensions/cpsection/modemconfiguration/Makefile.am
+++ b/extensions/cpsection/modemconfiguration/Makefile.am
@@ -4,3 +4,5 @@ sugar_PYTHON = 		\
 	__init__.py	\
 	model.py	\
 	view.py		
+
+nodist_sugar_PYTHON = config.py
diff --git a/extensions/cpsection/modemconfiguration/config.py b/extensions/cpsection/modemconfiguration/config.py
new file mode 100644
index 0000000..963616d
--- /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 969b5d9..2e4cd7d 100755
--- a/extensions/cpsection/modemconfiguration/model.py
+++ b/extensions/cpsection/modemconfiguration/model.py
@@ -1,4 +1,8 @@
 # Copyright (C) 2009 Paraguay Educa, Martin Abente
+# Copyright (C) 2010 Andres Ambrois <andresambrois at gmail.com>
+# Copyright (C) 2010 Anish Mangal   <anish at activitycentral.com>
+# Copyright (C) 2012 Ajay Garg      <ajay at activitycentral.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
@@ -18,10 +22,22 @@ import logging
 
 import dbus
 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()
 
@@ -98,3 +114,150 @@ 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
+        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:
+                self.append((codes[code], elem))
+            else:
+                self.append((code, elem))
+
+    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 4ce6c0d..68a4b3d 100644
--- a/extensions/cpsection/modemconfiguration/view.py
+++ b/extensions/cpsection/modemconfiguration/view.py
@@ -1,4 +1,7 @@
 # Copyright (C) 2009 Paraguay Educa, Martin Abente
+# Copyright (C) 2010 Andres Ambrois <andresambrois at gmail.com>
+# Copyright (C) 2010 Anish Mangal   <anish at activitycentral.com>
+# Copyright (C) 2012 Ajay Garg      <ajay at activitycentral.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
@@ -24,6 +27,11 @@ from sugar.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
 
@@ -63,6 +71,17 @@ class ModemConfiguration(SectionView):
         self.set_border_width(style.DEFAULT_SPACING)
         self.set_spacing(style.DEFAULT_SPACING)
         self._group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+        self._combo_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+
+        scrolled_win = gtk.ScrolledWindow()
+        scrolled_win.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_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 +90,85 @@ class ModemConfiguration(SectionView):
         self._text.set_width_chars(100)
         self._text.set_line_wrap(True)
         self._text.set_alignment(0, 0)
-        self.pack_start(self._text, False)
+        main_box.pack_start(self._text, False)
         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, expand=False)
+            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(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, False)
+            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, expand=False)
+        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, expand=False)
-        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, expand=False)
-        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, expand=False)
-        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, expand=False)
-        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, expand=False)
-        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)
+        label.show()
+
+        self._combo_group.add_widget(combobox_obj)
+        cell = gtk.CellRendererText()
+        cell.props.xalign = 0.5
+        cell.set_property('width-chars', 25)
+        combobox_obj.pack_start(cell)
+        combobox_obj.add_attribute(cell, 'text', 0)
+
+        box.pack_start(combobox_obj, False)
+        combobox_obj.show()
+        self._upper_box.pack_start(box, False)
+        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, expand=False)
+        entry_with_label_obj.show()
+
     def undo(self):
         self._model.undo()
 
@@ -113,11 +176,28 @@ 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):
+        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)
+
         settings = self._model.get_modem_settings()
         self._populate_entry(self._username_entry,
             settings.get('username', ''))
@@ -133,6 +213,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 = {}
-- 
1.7.10.2



More information about the Sugar-devel mailing list