diff options
-rw-r--r-- | .gitignore | 14 | ||||
-rw-r--r-- | AUTHORS | 4 | ||||
-rw-r--r-- | ChangeLog | 0 | ||||
-rw-r--r-- | Makefile.am | 10 | ||||
-rw-r--r-- | NEWS | 0 | ||||
-rw-r--r-- | README | 26 | ||||
-rw-r--r-- | TODO | 6 | ||||
-rwxr-xr-x | autogen.sh | 21 | ||||
-rw-r--r-- | configure.ac | 23 | ||||
-rw-r--r-- | po/.gitignore | 5 | ||||
-rw-r--r-- | po/LINGUAS | 1 | ||||
-rw-r--r-- | po/POTFILES.in | 5 | ||||
-rw-r--r-- | po/de.po | 161 | ||||
-rw-r--r-- | src/.gitignore | 4 | ||||
-rw-r--r-- | src/Makefile.am | 23 | ||||
-rw-r--r-- | src/ppm-modem-request.ui | 66 | ||||
-rw-r--r-- | src/ppm-provider-assistant.ui | 209 | ||||
-rw-r--r-- | src/ppm.ui | 320 | ||||
-rw-r--r-- | src/ppm/.gitignore | 2 | ||||
-rw-r--r-- | src/ppm/Makefile.am | 23 | ||||
-rw-r--r-- | src/ppm/__init__.py.in | 5 | ||||
-rw-r--r-- | src/ppm/modemproxy.py | 162 | ||||
-rw-r--r-- | src/ppm/provider.py | 78 | ||||
-rw-r--r-- | src/ppm/providerdb.py | 140 | ||||
-rw-r--r-- | src/prepaid-manager-applet.desktop.in | 6 | ||||
-rwxr-xr-x | src/prepaid-manager-applet.in | 3 | ||||
-rwxr-xr-x | src/prepaid-manager-applet.py | 563 |
27 files changed, 1880 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c81b2d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +config.log +config.status +aclocal.m4 +autom4te.cache/ +configure +COPYING +gnome-doc-utils.make +INSTALL +install-sh +Makefile +Makefile.in +missing +mkinstalldirs +prepaid-manager-applet-*.tar.gz @@ -0,0 +1,4 @@ +Prepaid Manager Applet code is developed by: + + Guido Günther <agx@sigxcpu.org> + diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ChangeLog diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..fb38ea7 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,10 @@ +NULL = + +SUBDIRS = src po + +DISTCLEANFILES = \ + intltool-extract \ + intltool-merge \ + intltool-update \ + gnome-doc-utils.make \ + $(NULL) @@ -0,0 +1,26 @@ +About +----- +prepaid-manager-applet aims to ease the handling of mobile internet connections +using GSM mobile prepaid cards on the GNOME Desktop. Such a SIM card can either +be in a mobile phone used as a modem, a usb 3g module (usb surf stick) or used +by the built in 3G chipset in your laptop/netbook. + +* It allows you to check the current balance and to top up the credit. +* It uses ModemManager to talk to your GSM chipset. + +Requirements +------------ +* ModemManager with ussd support (current git) +* mobile-broadband-provider-info with top-up support: + http://git.debian.org/?p=users/agx/mobile-broadband-provider-info.git +* pygtk2 >= 2.17 + +Project Page +------------ +https://honk.sigxcpu.org/piki/projects/ppm + +Contact +------- +Send comments, patches and suggestions to + +Guido Günther <agx@sigxcpu.org> @@ -0,0 +1,6 @@ +* Add raw mode to send arbitrary USSD commands +* Remember settings with gsettings +* Switch to PyGI +* Collect balance statistics in sqlitedb +* Handle multiple modems +* Add support for SMS top-up messages as used by some providers diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 0000000..ed5671b --- /dev/null +++ b/autogen.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Run this to generate all the initial makefiles, etc. + +srcdir=`dirname $0` +test -z "$srcdir" && srcdir=. +REQUIRED_AUTOMAKE_VERSION=1.9 +REQUIRED_INTLTOOL_VERSION=0.35.0 +PKG_NAME=prepaid-manager-applet + +(test -f $srcdir/configure.ac \ + && test -f $srcdir/src/prepaid-manager-applet.py) || { + echo -n "**Error**: Directory "\`$srcdir\'" does not look like the" + echo " top-level $PKG_NAME directory" + exit 1 +} + +which gnome-autogen.sh || { + echo "You need to install gnome-common from the GNOME git" + exit 1 +} +USE_GNOME2_MACROS=1 . gnome-autogen.sh diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..8c14dca --- /dev/null +++ b/configure.ac @@ -0,0 +1,23 @@ +# -*- Autoconf -*- + +AC_PREREQ([2.67]) +AC_INIT([prepaid-manager-applet], [0.0.0], + [http://honk.sigxcpu.org/projects/prepaid-manager.py]) +AM_INIT_AUTOMAKE([-Wno-portability]) + +IT_PROG_INTLTOOL([0.35.0]) + +GETTEXT_PACKAGE=prepaid-manager-applet +AC_SUBST(GETTEXT_PACKAGE) +AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE,"$GETTEXT_PACKAGE", [Gettext package]) +AM_GLIB_GNU_GETTEXT + +GNOME_DOC_INIT + +AC_CONFIG_FILES([ +Makefile +po/Makefile.in +src/Makefile +src/ppm/Makefile +]) +AC_OUTPUT diff --git a/po/.gitignore b/po/.gitignore new file mode 100644 index 0000000..c7d5b98 --- /dev/null +++ b/po/.gitignore @@ -0,0 +1,5 @@ +*.gmo +.intltool-merge-cache +Makefile.in.in +POTFILES +stamp-it diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 0000000..7673daa --- /dev/null +++ b/po/LINGUAS @@ -0,0 +1 @@ +de diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 0000000..0b74d48 --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,5 @@ +[type: gettext/glade]src/ppm.ui +[type: gettext/glade]src/ppm-modem-request.ui +[type: gettext/glade]src/ppm-provider-assistant.ui +src/prepaid-manager-applet.desktop.in +src/prepaid-manager-applet.py diff --git a/po/de.po b/po/de.po new file mode 100644 index 0000000..c6e2cfa --- /dev/null +++ b/po/de.po @@ -0,0 +1,161 @@ +# German translation of prepaid-manager-applet +# Copyright (C) 2010 Guido Günther <agx@sigxcpu.org> +# This file is distributed under the same license as the prepaid-manager-applet +# package. +# Guido Günther <agx@sigxcpu.org>, 2010 +# +msgid "" +msgstr "" +"Project-Id-Version: 0.0.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-11-17 18:32+0100\n" +"PO-Revision-Date: 2010-11-11 13:38+0100\n" +"Last-Translator: Guido Günther <agx@sigxcpu.org>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../src/ppm.ui.h:1 +msgid "Balance" +msgstr "Guthaben" + +#: ../src/ppm.ui.h:2 +msgid "Balance information from" +msgstr "Guthaben Information vom" + +#: ../src/ppm.ui.h:3 +msgid "Balance unknown" +msgstr "Guthaben unbekannt" + +#: ../src/ppm.ui.h:4 +msgid "Change provider" +msgstr "Provider wechseln" + +#: ../src/ppm.ui.h:5 +msgid "Check your prepaid cards balance." +msgstr "Überprüfen Sie das Guthaben auf ihrer Prepaid Karte." + +#: ../src/ppm.ui.h:6 +msgid "Code:" +msgstr "Code:" + +#: ../src/ppm.ui.h:7 +msgid "Enter code to top up credit to your prepaid card." +msgstr "Geben Sie den Code ein um Guthaben auf ihre Prepaid-Karte zu laden." + +#: ../src/ppm.ui.h:8 ../src/ppm-modem-request.ui.h:1 +#: ../src/prepaid-manager-applet.desktop.in.h:1 +msgid "Prepaid Manager" +msgstr "Prepaid Manager" + +#: ../src/ppm.ui.h:9 +msgid "Provider:" +msgstr "Provider:" + +#: ../src/ppm.ui.h:10 +msgid "Top up" +msgstr "Aufladen" + +#: ../src/ppm.ui.h:11 +msgid "Top up your prepaid accounts balance" +msgstr "" + +#: ../src/ppm.ui.h:12 +msgid "change" +msgstr "" + +#: ../src/ppm.ui.h:13 +msgid "check" +msgstr "" + +#: ../src/ppm.ui.h:14 +msgid "top up" +msgstr "Aufladen" + +#: ../src/ppm.ui.h:15 +msgid "unknown" +msgstr "unbekannt" + +#: ../src/ppm.ui.h:16 +msgid "unknown provider" +msgstr "unbekannter Provider" + +#: ../src/ppm-modem-request.ui.h:2 +msgid "Sending request to modem" +msgstr "Sende Anfrage an das Modem" + +#: ../src/ppm-modem-request.ui.h:3 +msgid "Waiting for reply..." +msgstr "Warte auf Antwort..." + +#: ../src/ppm-provider-assistant.ui.h:1 +msgid "Check your selection" +msgstr "Überprüfen Sie Ihre Auswahl" + +#: ../src/ppm-provider-assistant.ui.h:2 +msgid "Please select the country of your mobile broadband provider." +msgstr "" + +#: ../src/ppm-provider-assistant.ui.h:3 +msgid "Please select your mobile broadband provider." +msgstr "" + +#: ../src/ppm-provider-assistant.ui.h:4 +msgid "" +"Prepaid Manager couldn't autodetect your mobile broadband provider. The " +"following pages will guide you through the process of setting it up." +msgstr "" + +#: ../src/ppm-provider-assistant.ui.h:5 +msgid "Provider configuration" +msgstr "Provider Konfiguration" + +#: ../src/ppm-provider-assistant.ui.h:6 +msgid "Select your country" +msgstr "Wählen Sie ihr Land aus" + +#: ../src/ppm-provider-assistant.ui.h:7 +msgid "Select your provider" +msgstr "Wählen Sie ihren Provider aus" + +#: ../src/ppm-provider-assistant.ui.h:8 +msgid "Your mobile broadband provider is" +msgstr "Ihr Provider für den mobilen Breitbandanschluß ist" + +#: ../src/ppm-provider-assistant.ui.h:9 +msgid "in" +msgstr "in" + +#: ../src/prepaid-manager-applet.desktop.in.h:2 +msgid "Prepaid Manager Applet for GSM" +msgstr "Prepaid Manager Applet für GSM" + +#: ../src/prepaid-manager-applet.py:281 +msgid "Enable Modem?" +msgstr "Modem aktivieren?" + +#: ../src/prepaid-manager-applet.py:395 +#, python-format +msgid "" +"We can't find the information on how to query the account balance from your " +"provider '%s' in our database." +msgstr "" + +#: ../src/prepaid-manager-applet.py:398 +#, python-format +msgid "" +"We can't find any information about your provider with mcc '%s' and mnc '%s'." +msgstr "" + +#: ../src/prepaid-manager-applet.py:401 +#, python-format +msgid "" +"\n" +"\n" +"You can go to %s to learn how to provide that information." +msgstr "" + +#~ msgid "Statistics" +#~ msgstr "Statistiken" diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..e54c960 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,4 @@ +prepaid-manager-applet +prepaid-manager-applet.desktop +*.pyc + diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..cdb887a --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,23 @@ +SUBDIRS = ppm + +bin_SCRIPTS_IN = prepaid-manager-applet.in +bin_SCRIPTS = prepaid-manager-applet + +desktopdir = $(datadir)/applications +desktop_in_files = prepaid-manager-applet.desktop.in +desktop_DATA = $(desktop_in_files:.desktop.in=.desktop) + +pythondir = $(pkgdatadir) +python_DATA = $(PACKAGE).py + +EXTRA_DIST = $(python_DATA) $(desktop_in_files) $(ui_DATA) + +@INTLTOOL_DESKTOP_RULE@ + +uidir = $(pkgdatadir) +ui_DATA = $(wildcard $(srcdir)/*.ui) + +$(PACKAGE): $(srcdir)/$(PACKAGE).in + sed -e "s,::PACKAGE::,$(PACKAGE)," -e "s,::PYTHONDIR::,$(pkgdatadir)," < $< > $@ + +CLEANFILES = *.pyc $(desktop_DATA) $(bin_SCRIPTS) diff --git a/src/ppm-modem-request.ui b/src/ppm-modem-request.ui new file mode 100644 index 0000000..6ddd166 --- /dev/null +++ b/src/ppm-modem-request.ui @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkDialog" id="ppm_modem_request"> + <property name="border_width">5</property> + <property name="title" translatable="yes">Prepaid Manager</property> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="skip_taskbar_hint">True</property> + <property name="skip_pager_hint">True</property> + <property name="has_separator">False</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child> + <object class="GtkLabel" id="label_processing"> + <property name="visible">True</property> + <property name="label" translatable="yes">Sending request to modem</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">10</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkProgressBar" id="progressbar"> + <property name="visible">True</property> + <property name="activity_mode">True</property> + <property name="show_text">True</property> + <property name="text" translatable="yes">Waiting for reply...</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">10</property> + <property name="position">2</property> + </packing> + </child> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/src/ppm-provider-assistant.ui b/src/ppm-provider-assistant.ui new file mode 100644 index 0000000..f07314d --- /dev/null +++ b/src/ppm-provider-assistant.ui @@ -0,0 +1,209 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkListStore" id="liststore_providers"> + <columns> + <!-- column-name column_provider --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkListStore" id="liststore_countries"> + <columns> + <!-- column-name column_countries --> + <column type="gchararray"/> + <!-- column-name column_country_codes --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkAssistant" id="ppm_provider_assistant"> + <property name="border_width">12</property> + <property name="title">Provider Configuration</property> + <property name="modal">True</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="skip-pager-hint">True</property> + <property name="skip-taskbar-hint">True</property> + <signal name="prepare" handler="on_ppm_provider_assistant_prepare"/> + <signal name="cancel" handler="on_ppm_provider_assistant_cancel"/> + <signal name="close" handler="on_ppm_provider_assistant_close"/> + <child> + <placeholder/> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="label" translatable="yes">Prepaid Manager couldn't autodetect your mobile broadband provider. The following pages will guide you through the process of setting it up.</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="page_type">intro</property> + <property name="complete">True</property> + <property name="title" translatable="yes">Provider configuration</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox_countries"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="label_countries"> + <property name="visible">True</property> + <property name="label" translatable="yes">Please select the country of your mobile broadband provider.</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">10</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="treeview_countries"> + <property name="height_request">200</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="model">liststore_countries</property> + <property name="search_column">0</property> + <signal name="cursor_changed" handler="on_treeview_countries_changed"/> + <child> + <object class="GtkTreeViewColumn" id="treeviewcolumn1"> + <property name="title">Country</property> + <property name="sort_column_id">0</property> + <child> + <object class="GtkCellRendererText" id="renderer_countries"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="title" translatable="yes">Select your country</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox_providers"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="label_providers"> + <property name="visible">True</property> + <property name="label" translatable="yes">Please select your mobile broadband provider.</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">10</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="treeview_providers"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="model">liststore_providers</property> + <property name="search_column">0</property> + <signal name="cursor_changed" handler="on_treeview_providers_changed"/> + <child> + <object class="GtkTreeViewColumn" id="treeviewcolumn_providers"> + <property name="title">Providers</property> + <property name="sort_column_id">0</property> + <child> + <object class="GtkCellRendererText" id="renderer_providers"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="title" translatable="yes">Select your provider</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox_confirm"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="label_provider0"> + <property name="visible">True</property> + <property name="label" translatable="yes">Your mobile broadband provider is</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_provider"> + <property name="visible">True</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_country0"> + <property name="visible">True</property> + <property name="label" translatable="yes">in</property> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_country"> + <property name="visible">True</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="page_type">confirm</property> + <property name="complete">True</property> + <property name="title" translatable="yes">Check your selection</property> + </packing> + </child> + </object> +</interface> diff --git a/src/ppm.ui b/src/ppm.ui new file mode 100644 index 0000000..3e73f42 --- /dev/null +++ b/src/ppm.ui @@ -0,0 +1,320 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkDialog" id="ppm_dialog"> + <property name="border_width">5</property> + <property name="title" translatable="yes">Prepaid Manager</property> + <property name="type_hint">dialog</property> + <property name="has_separator">False</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child> + <object class="GtkNotebook" id="notebook1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkTable" id="table_balance"> + <property name="visible">True</property> + <property name="n_rows">3</property> + <property name="n_columns">3</property> + <property name="column_spacing">4</property> + <property name="row_spacing">4</property> + <child> + <object class="GtkLabel" id="label_provider"> + <property name="visible">True</property> + <property name="label" translatable="yes">Provider:</property> + </object> + <packing> + <property name="x_options"></property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_balance_provider_name"> + <property name="visible">True</property> + <property name="label" translatable="yes">unknown provider</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_provider_change"> + <property name="label" translatable="yes">change</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Change provider</property> + <signal name="clicked" handler="on_provider_change_clicked"/> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_balance_from"> + <property name="label" translatable="yes">Balance information from</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options"></property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_balance_timestamp"> + <property name="label" translatable="yes">unknown</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_balance_info"> + <property name="visible">True</property> + <property name="label" translatable="yes">Balance unknown</property> + <attributes> + <attribute name="style" value="normal"/> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_check_balance"> + <property name="label" translatable="yes">check</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Check your prepaid cards balance.</property> + <signal name="clicked" handler="on_balance_info_renew_clicked"/> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkLabel" id="tab_balance"> + <property name="visible">True</property> + <property name="label" translatable="yes">Balance</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table_top_up"> + <property name="visible">True</property> + <property name="n_rows">3</property> + <property name="n_columns">3</property> + <property name="column_spacing">4</property> + <property name="row_spacing">4</property> + <child> + <object class="GtkLabel" id="label_provider1"> + <property name="visible">True</property> + <property name="label" translatable="yes">Provider:</property> + </object> + <packing> + <property name="x_options"></property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_topup_provider_name"> + <property name="visible">True</property> + <property name="label" translatable="yes">unknown provider</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_provider_change1"> + <property name="label" translatable="yes">change</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_top_up"> + <property name="label" translatable="yes">top up</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Top up your prepaid accounts balance</property> + <signal name="clicked" handler="on_balance_top_up_clicked"/> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_code"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">Enter code to top up credit to your prepaid card.</property> + <property name="invisible_char">●</property> + <signal name="changed" handler="on_entry_code_insert"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_code"> + <property name="visible">True</property> + <property name="label" translatable="yes">Code:</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options"></property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_top_up_reply"> + <property name="visible">True</property> + <property name="label" translatable="no"></property> + </object> + <packing> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options"></property> + <property name="x_padding">5</property> + <property name="y_padding">5</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="tab_top_up"> + <property name="visible">True</property> + <property name="label" translatable="yes">Top up</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_close"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_close_clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button_close</action-widget> + </action-widgets> + </object> +</interface> diff --git a/src/ppm/.gitignore b/src/ppm/.gitignore new file mode 100644 index 0000000..1a38d62 --- /dev/null +++ b/src/ppm/.gitignore @@ -0,0 +1,2 @@ +__init__.py +*.pyc diff --git a/src/ppm/Makefile.am b/src/ppm/Makefile.am new file mode 100644 index 0000000..7649cbb --- /dev/null +++ b/src/ppm/Makefile.am @@ -0,0 +1,23 @@ +NULL = + +pythondir = $(pkgdatadir)/ppm/ +python_DATA_IN = __init__.py.in +python_DATA = \ + __init__.py \ + modemproxy.py \ + provider.py \ + providerdb.py \ + $(NULL) + + +EXTRA_DIST = $(python_DATA) $(python_DATA_IN) + +%.py: $(srcdir)/%.py.in + sed -e "s,::PACKAGE::,$(PACKAGE)," \ + -e "s,::PYLIBDIR::,$(pkgdatadir)," \ + -e "s,::GETTEXTDIR::,$(datadir)/locale," \ + -e "s,::DATADIR::,$(datadir)," \ + -e "s,::PKGDATADIR::,$(pkgdatadir)," \ + < $< > $@ + +CLEANFILES = *.pyc diff --git a/src/ppm/__init__.py.in b/src/ppm/__init__.py.in new file mode 100644 index 0000000..ab3c59d --- /dev/null +++ b/src/ppm/__init__.py.in @@ -0,0 +1,5 @@ +# Paths for prepaid-manager-applet + +gettext_app = '::PACKAGE::' +gettext_dir = '::GETTEXTDIR::' +ui_dir = '::PKGDATADIR::' diff --git a/src/ppm/modemproxy.py b/src/ppm/modemproxy.py new file mode 100644 index 0000000..352b539 --- /dev/null +++ b/src/ppm/modemproxy.py @@ -0,0 +1,162 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2010 Guido Guenther <agx@sigxcpu.org> +# 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 3 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import dbus +import dbus.glib +import dbus.service +import gobject + + +class ModemError(Exception): + def __init__(self, msg): + self.msg = msg + + def is_forbidden(self): + return [False, True][self.msg.find("Operation not allowed") != -1] + + def is_disabled(self): + return [False, True][self.msg.find("not enabled") != -1] + + +class ModemManagerProxy(gobject.GObject): + """Interface to ModemManager DBus API + @ivar request: current pending request to ModemManager + @type request: string + @ivar modem: dbus path of modem we're currently acting on + @type modem: string + """ + + MM_DBUS_SERVICE='org.freedesktop.ModemManager' + MM_DBUS_INTERFACE_MODEM_MANAGER='org.freedesktop.ModemManager' + MM_DBUS_OBJECT_MODEM_MANAGER='/org/freedesktop/ModemManager' + MM_DBUS_INTERFACE_MODEM='org.freedesktop.ModemManager.Modem' + MM_DBUS_INTERFACE_MODEM_GSM_CARD='org.freedesktop.ModemManager.Modem.Gsm.Card' + MM_DBUS_INTERFACE_MODEM_GSM_USSD='org.freedesktop.ModemManager.Modem.Gsm.Ussd' + + __gsignals__ = { + # Emitted when we got the new account balance from the provider + 'request-started': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + [object]), + 'request-finished': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + [object]), + } + + def __init__(self): + self.__gobject_init__() + self.bus = dbus.SystemBus() + self.request = None + self.reply_func = None + self.error_func = None + self.modem = None + self.obj = None + + def set_modem(self, modem): + self.modem = modem + self.obj = self.bus.get_object(self.MM_DBUS_SERVICE, self.modem) + + def mm_request(func): + def wrapped_f( self, *args, **kw) : + self.request = "%s" % func.func_name + if kw.has_key('reply_func'): + self.reply_func = kw['reply_func'] + if kw.has_key('error_func'): + self.error_func = kw['error_func'] + self.emit('request-started', self) + ret = func(self, *args, **kw) + return ret + wrapped_f.__name__= func.__name__ + wrapped_f.__doc__= func.__doc__ + return wrapped_f + + def mm_request_done(func): + def wrapped_f( self, *args, **kw): + self.emit('request-finished', self) + ret = func(self, *args, **kw) + self.reply_func = None + self.error_func = None + self.request = None + return ret + wrapped_f.__name__= func.__name__ + wrapped_f.__doc__= func.__doc__ + return wrapped_f + + def request_pending(self): + if self.request: + return True + else: + return False + + @mm_request_done + def handle_dbus_reply(self, *args): + if self.reply_func: + self.reply_func(*args) + + @mm_request_done + def handle_dbus_error(self, e): + if self.error_func: + me = ModemError("%s failed: %s" % (self.request, e)) + self.error_func(me) + + def get_modems(self): + modems = [] + proxy = self.bus.get_object(self.MM_DBUS_SERVICE, + self.MM_DBUS_OBJECT_MODEM_MANAGER) + mm = dbus.Interface(proxy, + dbus_interface=self.MM_DBUS_INTERFACE_MODEM_MANAGER) + ret = mm.EnumerateDevices() + for modem in ret: + modems.append(modem) + return modems + + def get_imsi(self): + card = dbus.Interface(self.obj, + dbus_interface=self.MM_DBUS_INTERFACE_MODEM_GSM_CARD) + try: + return card.GetImsi() + except dbus.exceptions.DBusException as msg: + raise ModemError("Getting IMSI failed: %s" % msg) + + def get_network_id(self): + imsi = self.get_imsi() + mcc = imsi[0:3] + mnc = imsi[3:5] + return (mcc, mnc) + + @mm_request + def ussd_initiate(self, command, reply_func=None, error_func=None): + ussd = dbus.Interface(self.obj, + dbus_interface=self.MM_DBUS_INTERFACE_MODEM_GSM_USSD) + return ussd.Initiate(command, + reply_handler=self.handle_dbus_reply, + error_handler=self.handle_dbus_error) + + @mm_request + def _modem__enable(self, enable, reply_func=None, error_func=None): + ussd = dbus.Interface(self.obj, + dbus_interface=self.MM_DBUS_INTERFACE_MODEM) + ussd.Enable(enable, + reply_handler=self.handle_dbus_reply, + error_handler=self.handle_dbus_error) + + def modem_enable(self, reply_func=None, error_func=None): + self._modem__enable(True) + + def modem_disable(self, reply_func=None, error_func=None): + self._modem_enable(False) + +gobject.type_register(ModemManagerProxy) diff --git a/src/ppm/provider.py b/src/ppm/provider.py new file mode 100644 index 0000000..484270b --- /dev/null +++ b/src/ppm/provider.py @@ -0,0 +1,78 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2010 Guido Guenther <agx@sigxcpu.org> +# 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 3 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import logging + +class ProviderError(Exception): + def __init__(self, msg): + self.msg = msg + + +class Provider(object): + """Keeps the information on howto top up the ballance and how to query the + current ballance""" + + def __init__(self, country, name): + self.country = country + self.name = name + self.fetch_balance_cmds = {} + self.top_up_cmds = {} + self.balance = None + logging.debug("New provider: %s, %s", country, name) + + def add_fetch_balance_cmd(self, cmd): + self.fetch_balance_cmds.update(cmd) + logging.debug("Adding balance check %s" % cmd) + + def add_top_up_cmd(self, cmd): + self.top_up_cmds.update(cmd) + logging.debug("Adding top up %s" % cmd) + + def has_fetch_balance_cmd(self): + # Only USSD for now + if self.fetch_balance_cmds.has_key('ussd'): + return True + else: + return False + + def has_top_up_cmd(self): + # Only USSD for now + if self.top_up_cmds.has_key('ussd'): + return True + else: + return False + + def fetch_balance(self, mm, reply_func=None, error_func=None): + if self.has_fetch_balance_cmd(): + mm.ussd_initiate (self.fetch_balance_cmds['ussd'], + reply_func=reply_func, + error_func=error_func) + return True + else: + return False + + def top_up(self, mm, code, reply_func=None, error_func=None): + if self.has_top_up_cmd(): + cmd = self.top_up_cmds['ussd'][0].replace( + self.top_up_cmds['ussd'][1], + code) + logging.debug("Top up cmd: %s", cmd) + mm.ussd_initiate (cmd, reply_func=reply_func, error_func=error_func) + return True + else: + return False diff --git a/src/ppm/providerdb.py b/src/ppm/providerdb.py new file mode 100644 index 0000000..a1bc394 --- /dev/null +++ b/src/ppm/providerdb.py @@ -0,0 +1,140 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2010 Guido Guenther <agx@sigxcpu.org> +# 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 3 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import logging +from lxml import etree + +from provider import Provider + +class ProviderDB(object): + """Proxy to mobile broadband provider database""" + + provider_info = os.getenv('PPM_PROVIDER_DB', + '/usr/share/mobile-broadband-provider-info/' + 'serviceproviders.xml') + country_codes = '/usr/share/zoneinfo/iso3166.tab' + + def __init__(self): + self.__tree = None + self.__countries = {} + + @property + def tree(self): + if self.__tree: + return self.__tree + else: + self.__tree = etree.parse(self.provider_info) + return self.__tree + + @property + def countries(self): + if self.__countries: + return self.__countries + else: + self._tree = self._load_countries() + return self.__countries + + def _load_countries(self): + try: + for line in file(self.country_codes, 'r'): + if line[0] != '#': + (code, country) = line.split('\t',2) + self.__countries[code.lower()] = country.strip() + except IOError as msg: + logging.warning("Loading country code database failed: %s" % msg) + + def _fill_provider_info(self, provider_elem): + """Fill a provider object with data from the XML""" + name = provider_elem.xpath("./name")[0].text + country = provider_elem.getparent().attrib['code'] + gsm_elem = provider_elem.xpath("./gsm") + provider = Provider(country = country, + name = name) + if gsm_elem: + self._fill_balance_check_cmd(gsm_elem[0], provider) + self._fill_top_up_cmd(gsm_elem[0], provider) + return provider + + def _fill_balance_check_cmd(self, xmlelemnt, provider): + """Fetch balance check method from XML and add it to the provider""" + for child in xmlelemnt.iter(tag='balance-check'): + check_types = child.getchildren() + for t in check_types: + if t.tag == 'ussd': + sequence = t.text + provider.add_fetch_balance_cmd({'ussd': sequence}) + if t.tag == 'sms': + number = t.text + text = t.attrib['text'] + provider.add_fetch_balance_cmd({'sms': (number, text)}) + + def _fill_top_up_cmd(self, xmlelement, provider): + for child in xmlelement.iter(tag='balance-top-up'): + check_types = child.getchildren() + for t in check_types: + if t.tag == 'ussd': + sequence = t.text + replacement = t.attrib['replacement'] + provider.add_top_up_cmd({'ussd': [sequence, replacement]}) + if t.tag == 'sms': + number = t.text + text = t.attrib['text'] + provicer.add_top_up_cmd({'sms': (number, text)}) + + def get_providers(self, mcc, mnc): + """ + Get possible providers for the current mcc and mnc from the database + """ + path = "//network-id[@mcc='%s' and @mnc='%s']" % (mcc, mnc) + searcher = etree.ETXPath(path) + providers = [] + + for r in searcher(self.tree): + provider_elem = r.getparent().getparent() + providers.append(self._fill_provider_info(provider_elem)) + return providers + + def get_provider(self, country_code, name): + path = "//country[@code='%s']/provider[name='%s']" % (country_code, name) + searcher = etree.ETXPath(path) + + for r in searcher(self.tree): + return self._fill_provider_info(r) + return None + + def get_country_codes(self): + path = "/serviceproviders/country" + searcher = etree.ETXPath(path) + + for r in searcher(self.tree): + yield r.attrib['code'] + + def get_countries(self): + for code in self.get_country_codes(): + try: + yield (self.countries[code], code) + except KeyError: + yield (None, code) + + def get_providers_by_code(self, country_code): + path = ("/serviceproviders/country[@code='%s']/provider/name" % + country_code) + searcher = etree.ETXPath(path) + + for r in searcher(self.tree): + yield r.text diff --git a/src/prepaid-manager-applet.desktop.in b/src/prepaid-manager-applet.desktop.in new file mode 100644 index 0000000..4b568c1 --- /dev/null +++ b/src/prepaid-manager-applet.desktop.in @@ -0,0 +1,6 @@ +[Desktop Entry] +_Name=Prepaid Manager +_Comment=Prepaid Manager Applet for GSM +Exec=prepaid-manager-applet +Terminal=false +Type=Application diff --git a/src/prepaid-manager-applet.in b/src/prepaid-manager-applet.in new file mode 100755 index 0000000..192cc22 --- /dev/null +++ b/src/prepaid-manager-applet.in @@ -0,0 +1,3 @@ +#!/bin/sh + +exec python "::PYTHONDIR::/::PACKAGE::.py" "$@" diff --git a/src/prepaid-manager-applet.py b/src/prepaid-manager-applet.py new file mode 100755 index 0000000..e3957cb --- /dev/null +++ b/src/prepaid-manager-applet.py @@ -0,0 +1,563 @@ +#!/usr/bin/python +# vim: set fileencoding=utf-8 : +# +# (C) 2010 Guido Guenther <agx@sigxcpu.org> +# 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 3 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + + +import dbus +import dbus.mainloop.glib +import gettext +import gobject +import glib +import gtk +import locale +import logging +import os +import time + +import ppm +from ppm.modemproxy import (ModemManagerProxy, ModemError) +from ppm.provider import Provider +from ppm.providerdb import ProviderDB + +# The controller receives input and initiates a response by making calls on model +# objects. A controller accepts input from the user and instructs the model and +# view to perform actions based on that input. +class PPMController(gobject.GObject): + """ + @ivar providers: the possible providers + @ivar provider: current provider + """ + + __gsignals__ = { + # Emitted when we got the new account balance from the provider + 'balance-info-fetched': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + [object]), + } + + def _connect_mm_signals(self): + self.mm.connect('request-started', self.on_mm_request_started) + self.mm.connect('request-finished', self.on_mm_request_finished) + + def __init__(self): + self.__gobject_init__() + self.mm = None + self.provider = None + self.view = None + self.providerdb = ProviderDB() + + def get_account_balance(self): + return provider.balance + + def fetch_balance_info(self): + """Fetch the current account balance from the network""" + if not self.provider.fetch_balance(self.mm, + reply_func=self.on_balance_info_fetched, + error_func=self.on_modem_error): + self.view.show_provider_balance_info_missing(self.provider) + logging.error("No idea how to fetch account information for " + "%s in %s.", self.provider.name, self.provider.country) + + def top_up_balance(self): + code = self.view.get_top_up_code() + if not self.provider.top_up(self.mm, code, + reply_func=self.on_balance_topped_up, + error_func=self.on_modem_error): + self.view.show_provider_top_up_info_missing(self.provider) + logging.error("No idea how to top up balance for " + "%s in %s.", self.provider.name, self.provider.country) + + def set_provider(self, provider=None, country_code=None, name=None): + """Change the current provider to provider and inform the view""" + if name and country_code: + provider = self.providerdb.get_provider(country_code, name) + + if not provider: + raise Exception("No valid provider") + + self.provider = provider + self.view.update_provider_name(provider.name) + # FIXME: fetch last stored balance from db + + def _get_provider_from_mcc_mnc(self): + mcc, mnc = self.mm.get_network_id() + self.providers = self.providerdb.get_providers(mcc, mnc) + if self.providers: + if len(self.providers) > 1: + # More than one provider matching mcc/mnc, let user select + self.view.show_provider_assistant(self.providers) + else: + self.set_provider(self.providers[0]) + else: + self.view.show_provider_unknown(mcc, mnc) + + def init_current_provider(self): + try: + self._get_provider_from_mcc_mnc() + except ModemError as me: + logging.warning("Can't get network id: %s", me.msg) + if me.is_forbidden(): + # FIXME: fetch last provider from GSettings + self.view.show_provider_assistant() + return False + if not me.is_disabled(): + self.view.show_modem_error(me.msg) + return False + logging.info("modem not enabled") + if self.view.show_modem_enable() != gtk.RESPONSE_YES: + return False + else: + self.mm.modem_enable(reply_func=self.init_current_provider) + return True + + def setup(self): + logging.debug("Setting up") + + self.mm = ModemManagerProxy() + self._connect_mm_signals() + + modems = self.mm.get_modems() + if modems: + modem = modems[0] # FIXME: handle multiple modems + logging.debug("Using modem %s" % modem) + self.mm.set_modem(modem) + glib.timeout_add(500, self.init_current_provider) + else: + self.view.show_error("No modem found.") + self.quit() + return False + + def quit(self): + """Clean up""" + logging.debug("Quitting...") + self.view.close() + gtk.main_quit() + + def get_provider_countries(self): + return self.providerdb.get_countries() + + def get_provider_providers(self, country_code): + return self.providerdb.get_providers_by_code(country_code) + + def on_mm_request_started(self, obj, mm_proxy): + logging.debug("Started modem request: %s", mm_proxy.request) + self.view.show_modem_response() + + def on_mm_request_finished(self, obj, mm_proxy): + logging.debug("Finished modem request") + self.view.close_modem_response() + + def on_balance_info_fetched(self, balance, *args): + self.provider.balance = balance + self.emit('balance-info-fetched', self.provider.balance) + self.view.update_account_balance_information(self.provider.balance, + time.asctime()) + + def on_balance_topped_up(self, reply): + self.view.update_top_up_information(reply) + + def on_modem_error(self, e): + self.view.show_modem_error(e.msg) + logging.error(e.msg) + +gobject.type_register(PPMController) + + +class PPMObject(object): + """Dialog or window constructed via a GtkBuilder""" + def __init__(self, view, ui): + if view: + self.controller = view.controller + else: + self.controller = None + self._load_ui(ui) + + def _load_ui(self, ui): + """Load the user interfade description""" + self.builder = gtk.Builder() + self.builder.set_translation_domain(ppm.gettext_app) + self.builder.add_from_file(os.path.join(ppm.ui_dir, '%s.ui' % ui)) + self.builder.connect_signals(self) + + def _add_elem(self, name): + self.__dict__[name] = self.builder.get_object(name) + + def _add_elements(self, *args): + for name in args: + self._add_elem(name) + +# View +class PPMDialog(gobject.GObject, PPMObject): + + def _init_subdialogs(self): + self.provider_info_missing_dialog = PPMProviderInfoMissingDialog(self) + self.provider_assistant = PPMProviderAssistant(self) + self.modem_response = PPMModemResponse(self) + + def _setup_ui(self): + self.dialog = self.builder.get_object("ppm_dialog") + self._add_elements("label_balance_provider_name", + "label_topup_provider_name", + "label_balance_info", + "label_balance_timestamp", + "label_balance_from", + "entry_code", + "button_top_up", + "label_top_up_reply") + self._init_subdialogs() + + def __init__(self, controller): + self.__gobject_init__() + PPMObject.__init__(self, None, "ppm") + self.controller = controller + # Register ourself to the controller + self.controller.view = self + + self._setup_ui() + self.dialog.show() + + def close(self): + self.dialog.hide() + self.dialog.destroy() + + def get_top_up_code(self): + return self.entry_code.get_text().strip() + + def on_close_clicked(self, dummy): + self.controller.quit() + + def on_balance_top_up_clicked(self, dummy): + self.clear_top_up_information() + self.controller.top_up_balance() + + def on_balance_info_renew_clicked(self, dummy): + self.controller.fetch_balance_info() + + def on_provider_change_clicked(self, dummy): + # FIXME: allow to select provider + # and communicate the change to the controller + raise NotImplementedError + + def on_entry_code_insert(self, *args): + if self.entry_code.get_text(): + self.button_top_up.set_sensitive(True) + else: + self.button_top_up.set_sensitive(False) + + def update_provider_name(self, provider_name): + self.label_balance_provider_name.set_text(provider_name) + self.label_topup_provider_name.set_text(provider_name) + + def update_account_balance_information(self, balance_text, timestamp): + self.label_balance_info.set_text(balance_text) + self.label_balance_timestamp.set_text(timestamp) + self.label_balance_timestamp.show() + self.label_balance_from.show() + + def update_top_up_information(self, reply): + self.label_top_up_reply.set_text(reply) + + def clear_top_up_information(self): + self.label_top_up_reply.set_text("") + + def show_provider_balance_info_missing(self, provider): + self.provider_info_missing_dialog.balance_info_missing(provider) + + def show_provider_top_up_info_missing(self, provider): + self.provider_info_missing_dialog.top_up_info_missing(provider) + + def show_provider_unknown(self, mcc, mnc): + self.provider_info_missing_dialog.provider_unknown(mcc, mnc) + + def show_modem_error(self, msg): + dialog = gtk.MessageDialog(parent=self.dialog, + flags=gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT, + type=gtk.MESSAGE_ERROR, + buttons=gtk.BUTTONS_OK) + dialog.set_markup("Modem error: %s" % msg) + dialog.run() + dialog.hide() + + def show_modem_enable(self): + """Show dialog that asks if we should enable the modem""" + dialog = gtk.MessageDialog(parent=self.dialog, + flags=gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT, + type=gtk.MESSAGE_QUESTION, + buttons=gtk.BUTTONS_YES_NO) + dialog.set_markup(_("Enable Modem?")) + ret = dialog.run() + dialog.hide() + return ret + + def show_provider_assistant(self, providers=None): + self.provider_assistant.show(providers) + + def show_error(self, msg): + """show generic error""" + logging.debug(msg) + error = gtk.MessageDialog(parent=self.dialog, + flags=gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT, + type=gtk.MESSAGE_ERROR, + buttons=gtk.BUTTONS_OK) + error.set_markup(msg) + error.run() + error.hide() + + def show_modem_response(self): + self.modem_response.show() + + def close_modem_response(self): + self.modem_response.close() + + +gobject.type_register(PPMDialog) + +class PPMProviderAssistant(PPMObject): + + PAGE_INTRO, PAGE_COUNTRIES, PAGE_PROVIDERS, PAGE_CONFIRM = range(0, 4) + + def __init__(self, main_dialog): + PPMObject.__init__(self, main_dialog, "ppm-provider-assistant") + self.assistant = self.builder.get_object("ppm_provider_assistant") + self._add_elements("vbox_countries", + "treeview_countries", + "vbox_providers", + "treeview_providers", + "liststore_providers", + "label_country", + "label_provider") + self.assistant.set_transient_for(main_dialog.dialog) + self.liststore_countries = None + self.country_code = None + self.provider = None + self.possible_providers = None + + def _get_current_country_from_locale(self): + (l, enc) = locale.getlocale() + code = l.lower().split('_')[0] + logging.debug("Assuming your in country %s" % code) + return code + + def _select_country_row(self, iter): + path = self.liststore_countries.get_path(iter)[0] + treeselection = self.treeview_countries.get_selection() + treeselection.select_path(path) + self.treeview_countries.scroll_to_cell(path) + self.assistant.set_page_complete(self.vbox_countries, True) + + def _fill_liststore_countries(self): + """Fille the countries liststore with all known countries""" + lcode = self._get_current_country_from_locale() + if not self.liststore_countries: + self.liststore_countries = self.builder.get_object( + "liststore_countries") + for (country, code) in self.controller.get_provider_countries(): + if country is None: + country = code + iter = self.liststore_countries.append() + self.liststore_countries.set_value(iter, 0, country) + self.liststore_countries.set_value(iter, 1, code) + if code == lcode: + self.country_code = code + self._select_country_row(iter) + + def _providers_only_page_func(self, current_page): + if current_page < self.PAGE_PROVIDERS: + return self.PAGE_PROVIDERS + else: + return current_page+1 + + def _all_pages_func(self, current_page): + return current_page+1 + + def show(self, providers=None): + self.possible_providers = providers + self.provider = None + + if not self.possible_providers: + # No list of possible providers so allow to select the country first + self._fill_liststore_countries() + self.assistant.set_forward_page_func(self._all_pages_func) + else: + # List of possible providers given, all from the same country + self.country_code = self.possible_providers[0].country + self.assistant.set_forward_page_func(self._providers_only_page_func) + self.assistant.show() + + def close(self): + self.assistant.hide() + + def on_ppm_provider_assistant_cancel(self, obj): + logging.debug("Assistant canceled.") + self.close() + + def _fill_provider_liststore_by_country_code(self, country_code): + self.liststore_providers.clear() + for provider in self.controller.get_provider_providers(country_code): + iter = self.liststore_providers.append() + self.liststore_providers.set_value(iter, 0, provider) + + def _fill_provider_liststore_by_providers(self): + self.liststore_providers.clear() + for provider in self.possible_providers: + iter = self.liststore_providers.append() + self.liststore_providers.set_value(iter, + 0, + provider.name) + + def on_ppm_provider_assistant_prepare(self, obj, page): + if self.assistant.get_current_page() == self.PAGE_PROVIDERS: + if self.possible_providers: + self._fill_provider_liststore_by_providers() + else: + self._fill_provider_liststore_by_country_code(self.country_code) + elif self.assistant.get_current_page() == self.PAGE_CONFIRM: + self.label_country.set_text(self.country_code) + self.label_provider.set_text(self.provider) + + def on_treeview_countries_changed(self, obj): + self.assistant.set_page_complete(self.vbox_countries, True) + selection = self.treeview_countries.get_selection() + (model, iter) = selection.get_selected() + if not iter: + return + self.country_code = model.get_value(iter, 1) + + def on_treeview_providers_changed(self, obj): + self.assistant.set_page_complete(self.vbox_providers, True) + selection = self.treeview_providers.get_selection() + (model, iter) = selection.get_selected() + if not iter: + return + self.provider = model.get_value(iter, 0) + + def on_ppm_provider_assistant_close(self, obj): + logging.debug("Selected: %s %s", self.provider, self.country_code) + self.close() + self.controller.set_provider(name=self.provider, + country_code=self.country_code) + + +class PPMProviderInfoMissingDialog(object): + """ + If information about the provider is missing redirect the user to a webpage + that explains howto provide that information + """ + + wiki_url = ('<a href = \"http://live.gnome.org/NetworkManager/' + 'MobileBroadband/ServiceProviders\">website</a>') + + def __init__(self, main_dialog): + self.dialog = gtk.MessageDialog(parent=main_dialog.dialog, + flags=gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT, + type=gtk.MESSAGE_INFO, + buttons=gtk.BUTTONS_OK) + self.messages = { + 'balance_info_missing': + _("We can't find the information on how to query the " + "account balance from your provider '%s' in our database."), + 'top_up_info_missing': + _("We can't find the information on how to top up the " + "balance for your provider '%s' in our database."), + 'provider_unknown': + _("We can't find any information about your provider with " + "mcc '%s' and mnc '%s'.") + } + self.common_msg = _("\n\nYou can go to %s to learn how to provide that " + "information.") + + def _run(self, msg): + msg += self.common_msg % self.wiki_url + self.dialog.set_markup(msg) + self.dialog.run() + self.dialog.hide() + + def balance_info_missing(self, provider): + msg = self.messages['balance_info_missing'] % provider.name + self._run(msg) + + def top_up_info_missing(self, provider): + msg = self.messages['top_up_info_missing'] % provider.name + self._run(msg) + + def provider_unknown(self, mcc, mnc): + msg = self.messages['provider_unknown'] % (mcc, mnc) + self._run(msg) + + +class PPMModemResponse(PPMObject): + """Dialog shown while waiting for a response from the modem""" + def __init__(self, main_dialog): + PPMObject.__init__(self, main_dialog, "ppm-modem-request") + self.dialog = self.builder.get_object("ppm_modem_request") + self._add_elements("progressbar") + self.dialog.set_transient_for(main_dialog.dialog) + self.timer = None + + def show(self): + self.timer = glib.timeout_add(50, self.do_progress, + priority=glib.PRIORITY_HIGH) + self.dialog.show() + + def close(self): + if self.timer: + glib.source_remove(self.timer) + self.timer = None + self.dialog.hide() + + def do_progress(self): + self.progressbar.pulse() + return True + + +def setup_i18n(): + locale.setlocale(locale.LC_ALL, '') + gettext.install(ppm.gettext_app, ppm.gettext_dir) + gettext.bindtextdomain(ppm.gettext_app, ppm.gettext_dir) + locale.bindtextdomain(ppm.gettext_app, ppm.gettext_dir) + _ = gettext.gettext + logging.debug('Using locale: %s', locale.getlocale()) + + +def setup_dbus(): + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + +def main(): + logging.basicConfig(level=logging.DEBUG, + format='ppm: %(levelname)s: %(message)s') + + setup_dbus() + setup_i18n() + + controller = PPMController() + main_dialog = PPMDialog(controller) + glib.timeout_add(1, controller.setup) + + gtk.main() + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + logging.debug("Received KeyboardInterrupt. Exiting application.") + except SystemExit: + raise |