Fix zimbie process. Add more logging.
[ussd-widget] / ussd-widget / src / usr / lib / hildon-desktop / ussd-widget.py
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 ## This program is free software; you can redistribute it and/or modify
5 ## it under the terms of the GNU General Public License as published
6 ## by the Free Software Foundation; version 2 and higer.
7 ##
8 ## Guseynov Alexey (kibergus bark-bark gmail.com) 2010
9
10 import gobject
11 import gtk
12 import hildon
13 import hildondesktop
14 import os
15 import cairo
16 import pango
17 import time
18 import re
19 import gettext
20 import fcntl
21 import dbus
22 import subprocess
23 import gsmdecode
24 import sys
25 from dbus.mainloop.glib import DBusGMainLoop
26
27 # Would be truncated on every reboot, and shouldn't write 
28 # anythong if things go right way so it is OK not to have logrotate
29 log = open("/var/log/ussd-widget.log")
30 print >> sys.stderr, "Writing log to /var/log/ussd-widget.log"
31
32 try :
33     t = gettext.translation('ussd-widget', '/usr/share/locale')
34     _ = t.ugettext
35 except IOError:
36     print >> log, "Translation file for your language not found"
37     def retme(arg):
38         return arg
39     _ = retme
40
41 ussd_languages = ["German", "English", "Italian", "French", "Spanish", "Dutch", "Swedish", "Danish", "Portuguese", "Finnish", "Norwegian", "Greek", "Turkish", "Reserved1", "Reserved2", "Unspecified"]
42 ussd_languages_localized = [_("German"), _("English"), _("Italian"), _("French"), _("Spanish"), _("Dutch"), _("Swedish"), _("Danish"), _("Portuguese"), _("Finnish"), _("Norwegian"), _("Greek"), _("Turkish"), _("Reserved1"), _("Reserved2"), _("Unspecified")]
43
44 # TODO Cutt off too long messages and show them in separate dialog
45 # how TODO widget vertical minimum size policy
46
47 class USSD_Controller:
48     def __init__( self, widget ) :
49         self.widget = widget
50         # number, parser, chain, interval, regexp, width, execute_at_start,
51         # retry pattern, font, name, language, show_message_box,
52         # message_box_parser, additional arguments, regexp group, 
53         # use SMS listener, SMS number, SMS regexp, SMS timeout
54         self.default_config = ["", "", "", 0, "", 0, True, [],\
55             pango.FontDescription("Nokia Sans 18"), _("Click to update"),\
56             15, False, "", "", 1, False, "", "", 60]
57         self.config = self.default_config
58         self.timeout_version = 0
59         self.retry_version = 0
60         self.retry_state = 0
61         self.sms_counter = 0
62         self.sms_reply = ""
63
64     def save_config( self ) :
65         configname = os.getenv("HOME")+"/.ussdWidget.conf"
66         # Aquire lock
67         lockf = open(configname+".lock", 'a')
68         fcntl.flock(lockf,fcntl.LOCK_EX)
69
70         oldconfig=""
71         try:
72             fconfig = open(configname,"r")
73             #Read configuration of other instances
74             my_section = False
75             for line in fconfig :
76                 if line[0] == '%':
77                     my_section = line[1:].strip() == self.id
78                 if not my_section:
79                     oldconfig += line
80             fconfig.close()
81         except:
82             print >> log, _("Couldn't read previous config")
83
84         fconfig = open(configname,"w")
85         fconfig.seek(0)
86         fconfig.write(oldconfig)
87         fconfig.write("%"+self.id+"\n");
88         fconfig.writelines(["#USSD query to be run by widget\n", "number="+self.config[0], "\n"])
89         fconfig.writelines(["#Parser command for widget\n", "parser="+self.config[1], "\n"])
90         fconfig.writelines(["#Parser command for banner\n", "parser_box="+self.config[12], "\n"])
91         fconfig.writelines(["#Chain command\n", "chain="+self.config[2], "\n"])
92         fconfig.writelines(["#Update interval in minutes\n", "interval="+str(self.config[3]), "\n"])
93         fconfig.writelines(["#RegExp pattern\n", "regexp="+self.config[4], "\n"])
94         fconfig.writelines(["#Widget width\n", "width="+str(self.config[5]), "\n"])
95         fconfig.writelines(["#Execute query at start\n", "query_at_start="+str(self.config[6]), "\n"])
96         fconfig.writelines(["#Retry pattern\n"])
97         fconfig.write("retry=")
98         first = True
99         for i in self.config[7]:
100             if not first:
101                 fconfig.write ("-")
102             fconfig.write(str(i))
103             first = False
104         fconfig.write("\n")
105         fconfig.writelines(["#Font description\n", "font="+self.config[8].to_string(), "\n"])
106         fconfig.writelines(["#Font color\n", "text_color="+self.widget.get_text_color().to_string(), "\n"])
107         fconfig.writelines(["#Background color\n", "bg_color="+self.widget.get_bg_color().to_string(), "\n"])
108         fconfig.writelines(["#Widget name\n", "name="+self.config[9], "\n"])
109         fconfig.writelines(["#Show banner\n", "show_box="+str(self.config[11]), "\n"])
110         fconfig.writelines(["#USSD reply language\n", "language="+str(self.config[10]), "\n"])
111         fconfig.writelines(["#Additional ussdquery.py arguments\n", "args="+self.config[13], "\n"])
112         fconfig.writelines(["#Regexp matching group\n", "reggroup="+str(self.config[14]), "\n"])
113         fconfig.writelines(["#Use SMS listener\n", "listen_sms="+str(self.config[15]), "\n"])
114         fconfig.writelines(["#Number,from which SMS should come\n", "sms_number="+self.config[16], "\n"])
115         fconfig.writelines(["#SMS RegExp pattern\n", "sms_regexp="+self.config[17], "\n"])
116         fconfig.writelines(["#SMS timeout\n", "sms_timeout="+str(self.config[18]), "\n"])
117         fconfig.close()
118
119         fcntl.flock(lockf,fcntl.LOCK_UN)
120         lockf.close()
121
122     def get_config(self):
123         return self.config
124
125     def read_config( self, id ):
126         try :
127             self.id = id
128             config = open(os.getenv("HOME")+"/.ussdWidget.conf","r")
129
130             error = False
131             i = 0
132             my_section = False
133             for line in config :
134                 i += 1 
135                 if line[0] == '#':
136                     continue
137                 if line[0] == '%':
138                     my_section = line[1:].strip() == id
139                     continue
140
141                 if not my_section:
142                     # This is config for another instace
143                     continue
144
145                 line=line.split('=', 1)
146                 
147                 if len(line) != 2 :
148                     error = True
149                     print >> log, _("Error reading config on line %(line)d. = or # expected.")%{"line":i}
150                     continue
151                 if line[0] == "number" :
152                     self.config[0] = line[1].strip()
153                 elif line[0] == "parser" :
154                     self.config[1] = line[1].strip()
155                 elif line[0] == "parser_box" :
156                     self.config[12] = line[1].strip()
157                 elif line[0] == "chain" :
158                     self.config[2] = line[1].strip()
159                 elif line[0] == "interval" :
160                     try:
161                         self.config[3] = int(line[1].strip())
162                     except:
163                         error = True
164                         print >> log, _("Error reading config on line %(line)d. Integer expected.")%{"line":i}
165                         continue
166                 elif line[0] == "regexp" :
167                     self.config[4] = line[1].strip()
168                 elif line[0] == "width" :
169                     try:
170                         self.config[5] = int(line[1].strip())
171                     except:
172                         error = True
173                         print >> log, _("Error reading config on line %(line)d. Integer expected.")%{"line":i}
174                         continue
175                 elif line[0] == "query_at_start" :
176                     if line[1].strip() == "True" :
177                         self.config[6] = True
178                     else :
179                         self.config[6] = False
180                 elif line[0] == "retry" :
181                     line[1] = line[1].strip()
182                     if line[1] != "":
183                         line[1] = line[1].split("-")
184                         i = 0
185                         while i < len(line[1]) :
186                             try:
187                                 line[1][i] = int(line[1][i])
188                             except:
189                                 error = True
190                                 print >> log, _("Error reading config on line %(line)d. Integer expected.")%{"line":i}
191                             i += 1
192                         self.config[7] = line[1]
193                     else:
194                         self.config[7] = []
195                         continue
196                 elif line[0] == "font" :
197                     try:
198                         self.config[8] = pango.FontDescription(line[1].strip())
199                     except:
200                         error = True
201                         print >> log, _("Error reading config on line %(line)d. Pango font description expected.")%{"line":i}
202                         continue
203                 elif line[0] == "bg_color" :
204                     try:
205                         self.widget.set_bg_color(gtk.gdk.color_parse(line[1].strip()))
206                     except:
207                         error = True
208                         print >> log, _("Error reading config on line %(line)d. Expected color definition.")%{"line":i}
209                 elif line[0] == "text_color" :
210                     try:
211                         self.widget.set_text_color(gtk.gdk.color_parse(line[1].strip()))
212                     except:
213                         error = True
214                         print >> log, _("Error reading config on line %(line)d. Expected color definition.")%{"line":i}
215                 elif line[0] == "name" :
216                     self.config[9] = line[1].strip()
217                 elif line[0] == "show_box" :
218                     if line[1].strip() == "True" :
219                         self.config[11] = True
220                     else :
221                         self.config[11] = False
222                 elif line[0] == "language" :
223                     try:
224                         if int(line[1].strip()) >=0 and int(line[1].strip()) < len(ussd_languages):
225                             self.config[10] = int(line[1].strip())
226                         else:
227                             error = True
228                             print >> log, _("Error reading config on line %(line)d. Unknown language code.")%{"line":i}
229                     except:
230                         error = True
231                         print >> log, _("Error reading config on line %(line)d. Integer expected.")%{"line":i}
232                 elif line[0] == "args" :
233                     self.config[13] = line[1].strip()
234                 elif line[0] == "reggroup" :
235                     try:
236                         self.config[14] = int(line[1].strip())
237                     except:
238                         error = True
239                         print >> log, _("Error reading config on line %(line)d. Integer expected.")%{"line":i}
240                         continue
241                 elif line[0] == "listen_sms" :
242                     if line[1].strip() == "True" :
243                         self.config[15] = True
244                     else :
245                         self.config[15] = False
246                 elif line[0] == "sms_number" :
247                     self.config[16] = line[1].strip()
248                 elif line[0] == "sms_regexp" :
249                     self.config[17] = line[1].strip()
250                 elif line[0] == "sms_timeout" :
251                     try:
252                         self.config[18] = int(line[1].strip())
253                     except:
254                         error = True
255                         print >> log, _("Error reading config on line %(line)d. Integer expected.")%{"line":i}
256                         continue
257                 else :
258                     error = True
259                     print >> log, _("Error reading config on line %(line)d. Unexpected variable: ")%{"line":i}+line[0]
260                     continue 
261
262             config.close()
263
264             if error :
265                 self.widget.error = 1
266                 self.widget.set_text (_("Config error"), 5000)    
267
268             return self.config
269         except  IOError:
270             self.widget.error = 1
271             self.widget.set_text (_("Config error"), 0)
272             print >> log, _("IO error while reading config")
273
274             return self.default_config
275
276     def on_show_settings( self, widget ) :
277         dialog = UssdConfigDialog(self.config, self.widget.get_bg_color(), self.widget.get_text_color(), self.id)
278
279         while True:
280             if dialog.run() != gtk.RESPONSE_OK :
281                 dialog.destroy()
282                 return
283
284             test = check_regexp(dialog.regexp.get_text()) 
285             if test :
286                 dialog.on_error_regexp(test)
287                 continue
288
289             # Check, that we have ussd number
290             if not check_number(dialog.ussdNumber.get_text()):
291                 dialog.on_error_ussd_number()
292                 continue
293         
294             if not check_number(dialog.sms_number.get_text()):
295                 dialog.on_error_sms_number()
296                 continue
297
298             # Parse retry pattern
299             retry = dialog.retryEdit.get_text().strip()
300             if retry != "" :
301                 retry = retry.split("-")
302                 i = 0
303                 while i < len(retry) :
304                     try:
305                         retry[i] = int(retry[i])
306                     except:
307                         dialog.on_error_retry_pattern()
308                         break 
309                     i += 1
310             
311                 if i < len(retry):
312                     continue
313             else :
314                 retry = []
315
316             break
317
318         self.config = [
319             dialog.ussdNumber.get_text(), 
320             dialog.parser.get_text(), 
321             dialog.chain.get_text(), 
322             dialog.update_interval.get_value(), 
323             dialog.regexp.get_text(),
324             dialog.widthEdit.get_value(),
325             dialog.query_at_start.get_active(),
326             retry,
327             dialog.font,
328             dialog.wname.get_text(),
329             dialog.language.get_active(),
330             dialog.show_box.get_active(),
331             dialog.b_parser.get_text(),
332             dialog.args.get_text(),
333             dialog.reggroup.get_value(),
334             dialog.sms_listener.get_active(),
335             dialog.sms_number.get_text(),
336             dialog.sms_regexp.get_text(),
337             dialog.sms_timeout.get_value() 
338         ]
339
340         widget.set_bg_color(dialog.bg_color)
341         widget.set_text_color(dialog.text_color)
342
343         self.save_config()
344
345         widget.set_width(self.config[5])    
346         self.reset_timed_renew()
347         self.widget.label.modify_font(self.config[8])
348
349         dialog.destroy()
350
351         # Before running this function widget wasn't configured
352         if self.config == self.default_config:
353             self.widget.set_text(_("Click to update"))
354         return
355
356     def handle_sms(self, pdumsg, msgcenter, message, sendernumber):
357         # Timeout was recieved first
358         if self.sms_ready:
359             return
360
361         if self.config[16] == "" or self.config[16] == sendernumber:
362             pdu = gsmdecode.decode_pdu (pdumsg)
363             if pdu != None :
364                 self.sms_reply += pdu['user_data']
365                 if not pdu['part']:
366                     if self.config[17] == "" or re.search( self.config[17], message, re.MULTILINE | re.UNICODE ):
367                         self.sms_ready = True
368                         self.sms_signal.remove()
369                         self.process_reply()
370
371     def callback_ussd_data(self, source, condition, process):
372         if condition == gobject.IO_IN or condition == gobject.IO_PRI:
373             data = source.read()
374             self.cb_reply += data
375             return True
376
377         if condition == gobject.IO_ERR:
378             print >> log, "Communication error occured"
379             # This will force widget to show error message
380             self.cb_reply = ""
381
382         if condition == gobject.IO_HUP:
383             # Pipe is broken, so ussd-query.py is already terminating
384             # and we wouldn't wait fot it too long
385             retcode = process.wait()
386             if retcode != 0:
387                 self.cb_reply = ""
388
389         self.ussd_ready = True
390         self.process_reply()
391         return False
392
393     def callback_ussd_error(self, source, condition):
394         if condition == gobject.IO_IN or condition == gobject.IO_PRI:
395             self.error_message += source.read()
396         else:
397             if self.error_message != "":
398                 print >> log, self.error_message
399
400     def call_external_script(self, ussd_code, language):
401         self.cb_reply = "";
402         process = subprocess.Popen(
403                     ['/usr/bin/ussdquery.py', ussd_code, "-l", ussd_languages[language]] +\
404                         smart_split_string(self.config[13],"%","&"),
405                     stdin=subprocess.PIPE,
406                     stdout=subprocess.PIPE, 
407                     stderr=subprocess.PIPE)
408         process.stdin.close()
409         gobject.io_add_watch(
410                     p.stdout, gobject.IO_IN | gobject.IO_PRI | gobject.IO_HUP | gobject.IO_ERR,
411                     self.callback_ussd_data,
412                     process)
413         self.error_message = "";
414         gobject.io_add_watch(
415                     p.stderr, gobject.IO_IN | gobject.IO_PRI | gobject.IO_HUP | gobject.IO_ERR,
416                     self.callback_ussd_error)
417
418     def ussd_renew(self, widget, event):
419         if self.widget.processing == 0:
420             if self.config :
421                 widget.processing = 1
422                 widget.set_text(_("Processing"), 0)
423
424                 self.ussd_ready = False
425                 self.sms_ready = False
426                 self.sms_reply = ""
427
428                 if self.config[15]:
429                     self.sms_counter += 1
430                     self.retry_timer = gobject.timeout_add (1000*self.config[18], self.sms_timeout, self.sms_counter)
431                     
432                     self.bus = dbus.SystemBus()
433                     self.sms_signal = self.bus.add_signal_receiver(self.handle_sms, path='/com/nokia/phone/SMS',   dbus_interface='Phone.SMS', signal_name='IncomingSegment')
434
435                 self.call_external_script( self.config[0], self.config[10] )
436             else :
437                 widget.processing = 0
438                 widget.error = 1
439                 widget.set_text(_("No config"), 0)
440
441     def process_reply( self ):
442         if not self.ussd_ready or not self.sms_ready and self.config[15]:
443             return
444
445         reply = self.cb_reply.strip()
446         sms_reply = self.sms_reply.strip()
447         
448         if reply == "" or self.config[15] and sms_reply == "" :
449                 self.widget.error = 1
450             self.widget.set_text (_("Error"), 5000)
451             if self.retry_state == len(self.config[7]):
452                 self.retry_version += 1
453                 self.retry_state = 0
454             else :
455                 self.retry_timer = gobject.timeout_add (1000*self.config[7][self.retry_state], self.retry_renew, self.retry_version)
456                 self.retry_state += 1
457         else :
458             self.widget.error = 0
459             # Apply regexp
460             reresult1 = reresult2 = None
461             if self.config[4] != "":
462                 reresult1 = re.search( self.config[4], reply, re.MULTILINE | re.UNICODE )
463             if self.config[17] != "":
464                 reresult2 = re.search( self.config[17], sms_reply, re.MULTILINE | re.UNICODE )
465             w_reply = b_reply = reply
466             if self.widget.error == 0:
467                 # Pass to box parser
468                 if self.config[12] != "" and self.config[11]:
469                     try:
470                         p = subprocess.Popen(smart_split_string(self.config[12], reply, sms_reply, reresult1, reresult2), stdout=subprocess.PIPE)
471                         b_reply = p.communicate()[0].strip()
472                     except Exception, e:
473                         print >> log, _("Couldn't exec banner parser:")+str(e)
474                         self.widget.error = 1
475                 else:
476                     if self.config[4] != "":
477                         try :
478                             b_reply = reresult1.group( self.config[14] )
479                         except Exception, e:
480                             self.widget.error = 1
481                             b_reply = _("Group not found: \n") + reply
482                      
483                 # Pass to widget parser
484                 if self.config[1] != "":
485                     try:
486                         p = subprocess.Popen(smart_split_string(self.config[1], reply, sms_reply, reresult1, reresult2), stdout=subprocess.PIPE)
487                         w_reply = p.communicate()[0].strip()
488                     except Exception, e:
489                         print >> log, _("Couldn't exec widget parser:")+str(e)
490                         self.widget.error = 1
491                 else:
492                     if self.config[4] != "":
493                         try :
494                             w_reply = reresult1.group( self.config[14] )
495                         except Exception, e:
496                             self.widget.error = 1
497                             w_reply = _("Group not found: \n") + reply
498                 # Pass to chain
499                 if self.config[2] != "":
500                     try:
501                         p = subprocess.Popen(smart_split_string(self.config[2], reply, sms_reply, reresult1, reresult2))
502                     except Exception, e:
503                         print >> log, _("Couldn't exec chain:")+str(e)
504                         self.widget.error = 1
505             if self.config[11]:
506                 banner = hildon.hildon_banner_show_information (self.widget, "", b_reply)
507                 banner.set_timeout (5000)
508                 b_reply
509             self.widget.set_text(w_reply)
510         self.widget.processing = 0
511
512     def sms_timeout(self, version):
513         if version == self.sms_counter :
514             self.sms_reply = ""
515             self.sms_ready = True
516             self.sms_signal.remove()
517             self.process_reply()
518         return False
519
520     def timed_renew(self, version):
521         if version < self.timeout_version :
522             return False
523         self.ussd_renew(self.widget, None)
524         return True
525
526     def retry_renew(self,version):
527         if self.widget.error == 0 or self.widget.processing == 1 or version < self.retry_version :
528             return False
529         self.ussd_renew(self.widget, None)
530         return False
531
532     def reset_timed_renew (self) :
533         self.timeout_version += 1
534         if self.config[3] != 0 :
535             self.timer = gobject.timeout_add (60000*self.config[3], self.timed_renew, self.timeout_version)
536
537 class pHelpDialog(gtk.Dialog):
538     def __init__(self, heading, text):
539         gtk.Dialog.__init__(self, heading, None,
540             gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR,
541             (_("OK").encode("utf-8"), gtk.RESPONSE_OK))
542         label = gtk.Label(text)
543         label.set_line_wrap (True)
544         self.vbox.add(label)
545         self.show_all()
546         self.parent
547
548 class UssdConfigDialog(gtk.Dialog):
549     def __init__(self, config, bg_color, text_color, id):
550         gtk.Dialog.__init__(self, _("USSD widget : "+id), None, 
551             gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR,
552             (_("Save").encode("utf-8"), gtk.RESPONSE_OK))
553
554         self.font = config[8]
555         self.bg_color = bg_color
556         self.text_color = text_color
557
558         self.set_size_request(-1, 400)
559         self.ussdNumber = hildon.Entry(gtk.HILDON_SIZE_AUTO)
560         self.ussdNumber.set_text(config[0])
561         self.parser = hildon.Entry(gtk.HILDON_SIZE_AUTO)
562         self.parser.set_text(config[1])
563         self.b_parser = hildon.Entry(gtk.HILDON_SIZE_AUTO)
564         self.b_parser.set_text(config[12])
565
566         self.chain = hildon.Entry(gtk.HILDON_SIZE_AUTO)
567         self.chain.set_text(config[2])
568         self.update_interval = hildon.NumberEditor(0, 9999)
569         self.update_interval.set_value(config[3])
570         self.regexp = hildon.Entry(gtk.HILDON_SIZE_AUTO)
571         self.regexp.set_text(config[4])
572         self.widthEdit = hildon.NumberEditor(0, 1000)
573         self.widthEdit.set_value(config[5])
574         self.retryEdit = hildon.Entry(gtk.HILDON_SIZE_AUTO)
575         self.args = hildon.Entry(gtk.HILDON_SIZE_AUTO)
576         self.args.set_text(config[13])
577         self.reggroup = hildon.NumberEditor(0, 255)
578         self.reggroup.set_value(config[14])
579         
580         selector = hildon.TouchSelector(text=True)
581         for i in ussd_languages_localized:
582             selector.append_text(i)
583         self.language = hildon.PickerButton(gtk.HILDON_SIZE_AUTO, hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
584         self.language.set_selector(selector)
585         self.language.set_active(config[10])
586         self.language.set_title(_("USSD reply language"))
587         self.language.set_size_request(-1, -1)
588
589         self.wname = hildon.Entry(gtk.HILDON_SIZE_AUTO)
590         self.wname.set_text(config[9])
591         self.show_box = gtk.CheckButton(_("Enable banner. Parser:"))
592         self.show_box.connect("toggled", self.show_box_changed)
593         self.show_box.set_active(config[11])
594
595         text = ""
596         for i in config[7]:
597             if text != "":
598                 text += "-"
599             text += str(i)
600         self.retryEdit.set_text(text)
601
602         self.query_at_start = gtk.CheckButton(_("Execute query on start"))
603         self.query_at_start.set_active(config[6])
604
605         self.fontButton = gtk.Button(_("Font"))
606         self.fontButton.connect("clicked", self.on_show_font_selection)
607
608         self.colorButton = gtk.Button(_("Background color"))
609         self.colorButton.connect("clicked", self.on_show_color_selection)
610         self.textColorButton = gtk.Button(_("Text color"))
611         self.textColorButton.connect("clicked", self.on_show_text_color_selection)
612         
613         phelp = gtk.Button("?")
614         phelp.connect("clicked", self.on_show_phelp)
615         
616         bphelp = gtk.Button("?")
617         bphelp.connect("clicked", self.on_show_bphelp)
618
619         chelp = gtk.Button("?")
620         chelp.connect("clicked", self.on_show_chelp)
621
622         reghelp = gtk.Button("?")
623         reghelp.connect("clicked", self.on_show_reghelp)
624
625         retryhelp = gtk.Button("?")
626         retryhelp.connect("clicked", self.on_show_retryhelp)
627         
628         numberhelp = gtk.Button("?")
629         numberhelp.connect("clicked", self.on_show_number_help)
630
631         area = hildon.PannableArea()
632         self.vbox.add(area)
633         vbox = gtk.VBox()
634         area.add_with_viewport(vbox)
635         
636         numberBox = gtk.HBox()
637         numberLabel = gtk.Label(_("USSD number"))
638         numberLabel.set_alignment(0,0.6)
639         numberLabel.set_size_request(100, -1)
640         numberhelp.set_size_request(1, -1)
641         self.ussdNumber.set_size_request(200, -1)
642         numberBox.add(numberLabel)
643         numberBox.add(numberhelp)
644         numberBox.add(self.ussdNumber)
645         vbox.add(numberBox)
646
647         vbox.add(self.query_at_start)
648
649         nameBox = gtk.HBox()
650         nameLabel = gtk.Label(_("Name"))
651         nameLabel.set_alignment(0,0.6)
652         nameLabel.set_size_request(100, -1)
653         self.wname.set_size_request(200, -1)
654         nameBox.add(nameLabel)
655         nameBox.add(self.wname)
656         vbox.add(nameBox)
657
658         parserBox = gtk.HBox()
659         parserLabel = gtk.Label(_("Parser for widget"))
660         parserLabel.set_alignment(0,0.6)
661         parserLabel.set_size_request(200, -1)
662         phelp.set_size_request(10, -1)
663         parserBox.add(parserLabel)
664         parserBox.add(phelp)
665         vbox.add(parserBox)
666         vbox.add(self.parser)
667         
668         b_parserBox = gtk.HBox()
669         self.show_box.set_size_request(200, -1)
670         bphelp.set_size_request(10, -1)
671         b_parserBox.add(self.show_box)
672         b_parserBox.add(bphelp)
673         vbox.add(b_parserBox)
674         vbox.add(self.b_parser)
675         
676         chainBox = gtk.HBox()
677         chainLabel = gtk.Label(_("Chain"))
678         chainLabel.set_alignment(0,0.6)
679         chainLabel.set_size_request(200, -1)
680         chelp.set_size_request(10, -1)
681         chainBox.add(chainLabel)
682         chainBox.add(chelp)
683         vbox.add(chainBox)
684         vbox.add(self.chain)
685
686         regexpBox = gtk.HBox()
687         regexpLabel = gtk.Label(_("Regular expression"))
688         regexpLabel.set_alignment(0,0.6)
689         regexpLabel.set_size_request(200, -1)
690         regexpGroupLabel = gtk.Label(_("Group"))
691         regexpGroupLabel.set_size_request(1, -1)
692         reghelp.set_size_request(10, -1)
693         regexpBox.add(regexpLabel)
694         regexpBox.add(reghelp)
695         regexpBox.add(regexpGroupLabel)
696         vbox.add(regexpBox)
697         self.reggroup.set_size_request(1,-1);
698         self.regexp.set_size_request(250,-1);
699         regexpInputBox = gtk.HBox()
700         regexpInputBox.add(self.regexp)
701         regexpInputBox.add(self.reggroup)
702         vbox.add(regexpInputBox)        
703
704         widthBox = gtk.HBox()
705         widthLabel = gtk.Label(_("Max. width"))
706         widthLabel.set_alignment(0,0.6)
707         symbolsLabel = gtk.Label(_("symbols"))
708         widthLabel.set_size_request(140, -1)
709         self.widthEdit.set_size_request(50, -1)
710         symbolsLabel.set_size_request(40,-1)
711         widthBox.add(widthLabel)
712         widthBox.add(self.widthEdit)
713         widthBox.add(symbolsLabel)
714         vbox.add(widthBox)
715
716         updateBox = gtk.HBox()
717         updateLabel = gtk.Label(_("Update every"))
718         updateLabel.set_alignment(0,0.6)
719         minutesLabel = gtk.Label(_("minutes"))
720         updateLabel.set_size_request(140, -1)
721         self.update_interval.set_size_request(50, -1)
722         minutesLabel.set_size_request(40, -1)
723         updateBox.add(updateLabel)
724         updateBox.add(self.update_interval)
725         updateBox.add(minutesLabel)
726         vbox.add(updateBox)
727
728         retryBox = gtk.HBox()
729         retryLabel = gtk.Label(_("Retry pattern"))
730         retryLabel.set_alignment(0,0.6)
731         retryLabel.set_size_request(200, -1)
732         retryhelp.set_size_request(10, -1)
733         retryBox.add(retryLabel)
734         retryBox.add(retryhelp)
735         vbox.add(retryBox)
736         vbox.add(self.retryEdit)        
737         
738         argsLabel = gtk.Label(_("Additional ussdquery.py options"))
739         argsLabel.set_alignment(0,0.6)
740         vbox.add(argsLabel)
741         vbox.add(self.args)        
742         
743         viewBox = gtk.HBox()
744         viewBox.add(self.fontButton)
745         viewBox.add(self.textColorButton)
746         viewBox.add(self.colorButton)
747         vbox.add(viewBox)
748     
749         self.sms_box = gtk.VBox()    
750         self.sms_listener = gtk.CheckButton(_("Enable SMS listener."))
751         self.sms_listener.connect("toggled", self.sms_box_changed)
752         self.sms_listener.set_active(config[15])
753         vbox.add (self.sms_listener)
754
755         self.sms_number = hildon.Entry(gtk.HILDON_SIZE_AUTO)
756         self.sms_number.set_text(config[16])
757         smsNumberBox = gtk.HBox()
758         smsNumberLabel = gtk.Label(_("SMS number"))
759         smsNumberLabel.set_alignment(0,0.6)
760         smsNumberLabel.set_size_request(100, -1)
761         self.sms_number.set_size_request(200, -1)
762         smsNumberBox.add(smsNumberLabel)
763         smsNumberBox.add(self.sms_number)
764         self.sms_box.add(smsNumberBox)
765         
766         smsRegexpLabel = gtk.Label(_("Regular expression"))
767         smsRegexpLabel.set_alignment(0,0.6)
768         self.sms_box.add(smsRegexpLabel)
769         
770         self.sms_regexp = hildon.Entry(gtk.HILDON_SIZE_AUTO)
771         self.sms_regexp.set_text(config[17])
772         self.sms_box.add(self.sms_regexp)
773
774         self.sms_timeout = hildon.NumberEditor(0, 9999)
775         self.sms_timeout.set_value(config[18])
776         sms_timeout_box = gtk.HBox()
777         timeoutLabel = gtk.Label(_("Timeout"))
778         timeoutLabel.set_alignment(0,0.6)
779         secondsLabel = gtk.Label(_("seconds"))
780         timeoutLabel.set_size_request(140, -1)
781         self.sms_timeout.set_size_request(50, -1)
782         secondsLabel.set_size_request(40, -1)
783         sms_timeout_box.add(timeoutLabel)
784         sms_timeout_box.add(self.sms_timeout)
785         sms_timeout_box.add(secondsLabel)
786         self.sms_box.add(sms_timeout_box)
787         
788         vbox.add(self.sms_box)
789
790         vbox.add(gtk.Label(_("DO NOT CHANGE. Unspecified is what you want.")))
791         vbox.add(self.language)
792
793         self.show_all()
794         self.show_box_changed(None)
795         self.sms_box_changed(None)
796         self.parent
797
798     #============ Dialog helper functions =============
799     def on_show_phelp(self, widget):
800         dialog = pHelpDialog(_("Format help"), _("Reply would be passed to specified utility, output of utility would be shown to you on widget.\n       Format:\n% would be replaced by reply\n%N with N'th regexp matching group\n& would be replaced with sms content\n&N with N'th sms regexp group\n\\ invalidates special meaming of following symbol\n\" and ' work as usual\nspace delimits command line parameters of utility\n      Hint: use echo \"Your string %\" to prepend your string to reply."))
801         dialog.run()
802         dialog.destroy()
803
804     def on_show_bphelp(self, widget):
805         dialog = pHelpDialog(_("Format help"), _("Reply would be passed to specified utility, output of utility would be shown to you on banner.\n       Format:\n% would be replaced by reply\n%N with N'th regexp matching group\n& would be replaced with sms content\n&N with N'th sms regexp group\n\\ invalidates special meaming of following symbol\n\" and ' work as usual\nspace delimits command line parameters of utility\n      Hint: use echo \"Your string %\" to prepend your string to reply."))
806         dialog.run()
807         dialog.destroy()
808     
809     def on_show_chelp(self, widget):
810         dialog = pHelpDialog(_("Format help"), _("Reply would be passed to specified utility after parser utility. May be used for logging, statistics etc.\n       Format:\n% would be replaced by reply\n%N with N'th regexp matching group\n& would be replaced with sms content\n&N with N'th sms regexp group\n\\ invalidates special meaming of following symbol\n\" and ' work as usual\nspace delimits command line parameters of utility\n"))
811         dialog.run()
812         dialog.destroy()
813
814     def on_show_reghelp(self, widget):
815         dialog = pHelpDialog(_("Format help"), _("Standard python regexps. Use\n (.+?[\d\,\.]+)\n to delete everything after first number."))
816         dialog.run()
817         dialog.destroy()
818
819     def on_show_retryhelp(self, widget):
820         dialog = pHelpDialog(_("Format help"), _("Pauses between attemps (in seconds), delimited by -. For example 15-15-300 means \"In case of failure wait 15 seconds, try again, on failure wait 15 more secodns and try again, on failure make last attempt after 5 minutes\""))
821         dialog.run()
822         dialog.destroy()
823     
824     def on_show_number_help(self, widget):
825         dialog = pHelpDialog(_("Format help"), _("USSD number. To perform USSD menu navigation divide queries vith spacebars. For xample '*100# 1' means 1st entry in *100# menu."))
826         dialog.run()
827         dialog.destroy()
828     
829     def on_error_regexp(self, error):
830         dialog = pHelpDialog(_("Regexp syntax error"), error )
831         dialog.run()
832         dialog.destroy()
833
834     def on_error_ussd_number(self):
835         dialog = pHelpDialog(_("Incorrect USSD number"), _("USSD number should contain only digits, +, * or #") )
836         dialog.run()
837         dialog.destroy()
838
839     def on_error_retry_pattern(self):
840         dialog = pHelpDialog(_("Incorrect retry pattern"), _("Retry pattern should contain only numbers, delimited by -") )
841         dialog.run()
842         dialog.destroy()
843
844     def on_show_color_selection (self, event):
845         colorDialog = gtk.ColorSelectionDialog(_("Choose background color"))
846         colorDialog.colorsel.set_current_color(self.bg_color)
847         if colorDialog.run() == gtk.RESPONSE_OK :
848             self.bg_color = colorDialog.colorsel.get_current_color()
849         colorDialog.destroy()
850
851     def on_show_text_color_selection (self, event):
852         colorDialog = gtk.ColorSelectionDialog(_("Choose text color"))
853         colorDialog.colorsel.set_current_color(self.text_color)
854         if colorDialog.run() == gtk.RESPONSE_OK :
855             self.text_color = colorDialog.colorsel.get_current_color()
856         colorDialog.destroy()
857     
858     def on_show_font_selection (self, event):
859         fontDialog = gtk.FontSelectionDialog(_("Choose a font"))
860         fontDialog.set_font_name(self.font.to_string())
861
862         if fontDialog.run() != gtk.RESPONSE_OK :
863             fontDialog.destroy()
864             return
865
866         self.font = pango.FontDescription (fontDialog.get_font_name())
867             fontDialog.destroy()
868
869     def show_box_changed (self, event):
870         if self.show_box.get_active():
871             self.b_parser.show()
872         else:
873             self.b_parser.hide()
874
875     def sms_box_changed (self, event):
876         if self.sms_listener.get_active():
877             self.sms_box.show()
878         else:
879             self.sms_box.hide()
880
881 def smart_split_string (str, reply1, reply2, reres1 = None, reres2 = None) :
882     word = ""
883     result = []
884     # Is simbol backslashed?
885     bs = 0
886     # Quotes: 1 - ", 2 - ', 0 - no quotes
887     qs = 0
888     # Read out number
889     num = -1
890     # Current substitution simbol
891     subst = ''
892
893     for i in range(len(str)) :
894         if num>= 0:
895             if str[i] in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] :
896                 num *= 10
897                 num += int(str[i])
898                 continue
899             else:
900                 if subst == '&':
901                     if reres2 != None and num != 0:
902                         word += reres2.group(num)
903                     else:
904                         word += reply2
905                 else:
906                     if reres1 != None and num != 0:
907                         word += reres1.group(num)
908                     else:
909                         word += reply1
910                 ws = 0
911                 num = -1
912                 subst = ''
913                 # Delete backslash if it delimites usual numbers from % or &
914                 if str[i] == '\\' and i < len(str)-1 and str[i+1] in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] :
915                     continue
916         if bs == 0 and (str[i] == '"' and qs == 1 or str[i] == "'" and qs == 2) :
917             qs = 0
918         elif bs == 0 and qs == 0 and (str[i] == '"' or str[i] == "'") :
919             if str[i] == '"':
920                 qs = 1
921             else :
922                 qs = 2
923         elif bs == 0 and str[i] == '\\' :
924             bs = 1
925         elif bs == 0 and (str[i] == '%' or str[i] == '&') :
926             subst = str[i]
927             num = 0
928         else :
929             if bs == 1 and str[i] != '\\' and str[i] != '"' and str[i] != "'" :
930                 word += "\\"
931             if qs == 0 and (str[i] == " " or str[i] == "\t") :
932                 if word != "" :
933                     result.append(word)
934                     word = ""
935             else :
936                 word += str[i]
937               bs = 0 
938     
939     if subst == '&':
940         if reres2 != None and num != 0 and num != -1:
941             word += reres2.group(num)
942         else:
943             word += reply2
944     elif subst == '%':
945         if reres1 != None and num != 0 and num != -1:
946             word += reres1.group(num)
947         else:
948             word += reply1
949     if word != "" :
950         result.append(word)
951     return result 
952
953 def check_regexp(regexp):
954     try :
955         re.compile( regexp )
956     except Exception, e:
957             return str(e)
958     return False
959
960 def check_number(number):
961     for s in number :
962         if not (s in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "*", "#", " "]) :
963             return False
964     return True
965
966 #=============== The widget itself ================
967
968 def get_color(logicalcolorname):
969     settings = gtk.settings_get_default()
970     color_style = gtk.rc_get_style_by_paths(settings, 'GtkButton', 'osso-logical-colors', gtk.Button)
971     return color_style.lookup_color(logicalcolorname)
972
973 class UssdWidgetPlugin(hildondesktop.HomePluginItem):
974     def __init__(self):
975         hildondesktop.HomePluginItem.__init__(self)
976     
977         self.processing = 0
978         self.bg_color=gtk.gdk.color_parse('#000000')
979         self.text_color=gtk.gdk.color_parse('#ffffff')
980         self.error = 0
981         self.timeout_version = 0
982
983         colormap = self.get_screen().get_rgba_colormap()
984         self.set_colormap (colormap)
985
986         self.controller = USSD_Controller(self)
987          
988 # TODO Click event would be better
989         self.connect("button-press-event", self.controller.ussd_renew)
990
991         self.vbox = gtk.HBox()
992         self.add(self.vbox)
993
994         self.set_settings(True)
995         self.connect("show-settings", self.controller.on_show_settings)
996         self.label = gtk.Label("")
997         
998         self.vbox.add(self.label)
999         self.vbox.set_child_packing(self.label, False, False, 0, gtk.PACK_START)
1000         self.label.set_padding(15, 10)
1001         self.label.set_size_request(-1,-1)
1002         self.set_size_request(-1,-1)
1003         self.label.set_line_wrap (True)
1004
1005         self.vbox.show_all()
1006
1007         DBusGMainLoop(set_as_default=True)
1008         bus = dbus.SystemBus()
1009         signal = bus.add_signal_receiver(self.set_bg_color_text, path='/su/kibergus/ussd_widget',   dbus_interface='su.kibergus.ussd_widget', signal_name='set_bg_color')
1010         signal = bus.add_signal_receiver(self.set_text_color_text, path='/su/kibergus/ussd_widget',   dbus_interface='su.kibergus.ussd_widget', signal_name='set_text_color')
1011         signal = bus.add_signal_receiver(self.ussd_renew, path='/su/kibergus/ussd_widget',   dbus_interface='su.kibergus.ussd_widget', signal_name='renew')
1012
1013     def do_show(self):
1014         config = self.controller.read_config(self.get_applet_id())
1015         self.set_width(config[5])
1016         self.set_text(config[9])        
1017         if config[6]:
1018             self.controller.ussd_renew(self, None)
1019
1020         self.label.modify_font(config[8])
1021         self.controller.reset_timed_renew()
1022         hildondesktop.HomePluginItem.do_show(self)
1023     
1024     def error_return (self):
1025         if self.error == 1 and self.processing == 0:
1026             self.set_text(self.text)
1027         return False
1028
1029     # showfor =
1030     #    -1 - This is a permanent text message
1031     #    0  - This is service message, but it shouldn't be hidden automatically
1032     #    >0 - This is service message, show permament message after showfor milliseconds
1033     def set_text(self, text, showfor=-1):
1034         if showfor > 0 :
1035             # Show previous text after 5 seconds
1036             gobject.timeout_add (showfor, self.error_return)
1037         else :
1038             if showfor == -1 :
1039                 self.text = text
1040         
1041         config = self.controller.get_config()
1042         self.label.set_text(text)
1043
1044     def get_text(self):
1045         return self.text
1046
1047     def set_width(self, width):
1048         if width != 0:
1049             self.label.set_width_chars (width)
1050         else :
1051             self.label.set_width_chars(-1)
1052
1053     def ussd_renew(self, id):
1054         if id == self.get_applet_id():
1055             self.controller.ussd_renew(self, None)
1056
1057     def set_bg_color_text(self, id, color):
1058         if id == self.get_applet_id():
1059             try :
1060                 self.set_bg_color(gtk.gdk.color_parse(color.strip()))
1061             except:
1062                 print >> log, _("Unable to parse colour specification")
1063             self.queue_draw()
1064
1065     def set_text_color_text(self, id, color):
1066         if id == self.get_applet_id():
1067             try:
1068                 self.set_text_color(gtk.gdk.color_parse(color.strip()))
1069             except:
1070                 print >> log, _("Unable to parse colour specification")
1071             self.queue_draw()
1072
1073     def set_bg_color(self, color):
1074         self.bg_color = color
1075
1076     def get_bg_color(self):
1077         return self.bg_color
1078
1079     def set_text_color(self, color):
1080         self.label.modify_fg(gtk.STATE_NORMAL, color)        
1081         self.text_color = color
1082
1083     def get_text_color(self):
1084         return self.text_color
1085
1086     def _expose(self, event):
1087         cr = self.window.cairo_create()
1088
1089         # draw rounded rect
1090         width, height = self.label.allocation[2], self.label.allocation[3]
1091
1092         #/* a custom shape, that could be wrapped in a function */
1093         x0 = 0   #/*< parameters like cairo_rectangle */
1094         y0 = 0
1095
1096         radius = min(15, width/2, height/2)  #/*< and an approximate curvature radius */
1097
1098         x1 = x0 + width
1099         y1 = y0 + height
1100
1101         cr.move_to  (x0, y0 + radius)
1102         cr.arc (x0 + radius, y0 + radius, radius, 3.14, 1.5 * 3.14)
1103         cr.line_to (x1 - radius, y0)
1104         cr.arc (x1 - radius, y0 + radius, radius, 1.5 * 3.14, 0.0)
1105         cr.line_to (x1 , y1 - radius)
1106         cr.arc (x1 - radius, y1 - radius, radius, 0.0, 0.5 * 3.14)
1107         cr.line_to (x0 + radius, y1)
1108         cr.arc (x0 + radius, y1 - radius, radius, 0.5 * 3.14, 3.14)
1109
1110         cr.close_path ()
1111
1112         fg_color = get_color("ActiveTextColor")
1113
1114         if self.processing :
1115             bg_color=fg_color
1116         else :
1117             bg_color=self.bg_color
1118
1119         cr.set_source_rgba (bg_color.red / 65535.0, bg_color.green/65535.0, bg_color.blue/65535.0, 0.7)
1120         cr.fill_preserve ()
1121
1122         if self.error :
1123             cr.set_source_rgba (1.0, 0.0, 0.0, 0.5)
1124         else :
1125             cr.set_source_rgba (fg_color.red / 65535.0, fg_color.green / 65535.0, fg_color.blue / 65535.0, 0.7)
1126         cr.stroke ()
1127
1128     def do_expose_event(self, event):
1129         self.chain(event)
1130         self._expose (event)
1131         self.vbox.do_expose_event (self, event)
1132
1133 hd_plugin_type = UssdWidgetPlugin
1134
1135 # The code below is just for testing purposes.
1136 # It allows to run the widget as a standalone process.
1137 if __name__ == "__main__":
1138     plugin_id = "ussd-widget.console"
1139     if len(sys.argv) == 2:
1140         try:
1141             plugin_id = "ussd-widget.desktop-"+str(int(sys.argv[1]))
1142         except:
1143             print >> log, "Plugin id must be integer"
1144             sys.exit(-1)
1145
1146     import gobject
1147     gobject.type_register(hd_plugin_type)
1148     obj = gobject.new(hd_plugin_type, plugin_id=plugin_id)
1149     obj.show_all()
1150     gtk.main()