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

Ajay Garg ajay at activitycentral.com
Sun Aug 12 17:56:17 EDT 2012


Note that the patch is to be applied in full, and NOT over version-1 patch,
present at http://patchwork.sugarlabs.org/patch/1592/


Changes of version-2 over version-1 ::
======================================

a)
Corrected the makefile, for addition of "config.py".

Thanks Ruben, for the fix :)


b)
Now, in the countries' dropdown, the countries are shown sorted by name, and not by code.
Also, I don't think it is necessary to localize the countries-names as per say.
However, opinions are welcome :)

Thanks Ruben :)


c)
Removed horizontal scroll bar.

Thanks Ruben :)


ALSO, PLEASE NOTE :::::::
=========================

This patch is based, after the following two patches have been applied ::

   (i)
   http://patchwork.sugarlabs.org/patch/1642/

   (ii)
   http://patchwork.sugarlabs.org/patch/1643/


The first patch helps "truly" show the waiting-icon while the section-view
is loaded.

The second patch, solves converting the method-to-fetch-gsm-system-settings
from pseudo-asynchronous to purely asynchronous. 
This second patch is needed, as a result of first patch. The first patch worked for all,
except the "ModemConfiguration" section.

Thanks to Ruben for the motivation, of truly showing the waiting-cursor. :)




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



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       |    1 +
 extensions/cpsection/modemconfiguration/config.py  |   25 +++
 extensions/cpsection/modemconfiguration/model.py   |  175 +++++++++++++++++
 extensions/cpsection/modemconfiguration/view.py    |  200 +++++++++++++++++---
 4 files changed, 378 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..d15af14 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..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 8c900c9..dd4d9af 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()
 
@@ -99,3 +115,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 39a30c0..128156a 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_NEVER, 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,87 @@ 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', 30)
+
+        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 +178,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)
+
         self._model.get_modem_settings(self.populate_entries)
 
     def populate_entries(self, settings):
@@ -135,6 +217,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.4.4



More information about the Sugar-devel mailing list