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