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