a413ec5e046f349726752a4726389bbd00e0f98b
[gc-dialer] / gc_dialer / gc_dialer.py
1 #!/usr/bin/python2.5
2
3
4 """
5 Grandcentral Dialer
6 Python front-end to a wget script to use grandcentral.com to place outbound VOIP calls.
7 (C) 2008 Mark Bergman
8 bergman@merctech.com
9 """
10
11
12 import sys
13 import gc
14 import os
15 import threading
16 import time
17 import re
18
19 import warnings
20
21 import gobject
22 import gtk
23 import gtk.glade
24
25 try:
26         import hildon
27 except ImportError:
28         hildon = None
29
30 #try:
31 #       if hasattr(gtk, "Builder"):
32 #               #detected that this is not a legacy system
33 #               raise ImportError 
34 #       #Legacy support
35 #       import gtk.glade
36 #except ImportError:
37 #       gtk.glade = None
38
39 try:
40         import osso
41         try:
42                 import abook
43                 import evolution.ebook as evobook
44         except ImportError:
45                 abook = None
46                 evobook = None
47 except ImportError:
48         osso = None
49
50 try:
51         import doctest
52         import optparse
53 except ImportError:
54         doctest = None
55         optparse = None
56
57 from gcbackend import GCDialer
58
59 import socket
60
61
62 socket.setdefaulttimeout(5)
63
64
65 def makeugly(prettynumber):
66         """
67         function to take a phone number and strip out all non-numeric
68         characters
69
70         >>> makeugly("+012-(345)-678-90")
71         '01234567890'
72         """
73         uglynumber = re.sub('\D', '', prettynumber)
74         return uglynumber
75
76
77 def makepretty(phonenumber):
78         """
79         Function to take a phone number and return the pretty version
80          pretty numbers:
81                 if phonenumber begins with 0:
82                         ...-(...)-...-....
83                 if phonenumber begins with 1: ( for gizmo callback numbers )
84                         1 (...)-...-....
85                 if phonenumber is 13 digits:
86                         (...)-...-....
87                 if phonenumber is 10 digits:
88                         ...-....
89         >>> makepretty("12")
90         '12'
91         >>> makepretty("1234567")
92         '123-4567'
93         >>> makepretty("2345678901")
94         '(234)-567-8901'
95         >>> makepretty("12345678901")    
96         '1 (234)-567-8901'
97         >>> makepretty("01234567890")
98         '+012-(345)-678-90'
99         """
100         if phonenumber is None:
101                 return ""
102
103         if len(phonenumber) < 3:
104                 return phonenumber
105
106         if phonenumber[0] == "0":
107                 prettynumber = ""
108                 prettynumber += "+%s" % phonenumber[0:3]
109                 if 3 < len(phonenumber):
110                         prettynumber += "-(%s)" % phonenumber[3:6]
111                         if 6 < len(phonenumber):
112                                 prettynumber += "-%s" % phonenumber[6:9]
113                                 if 9 < len(phonenumber):
114                                         prettynumber += "-%s" % phonenumber[9:]
115                 return prettynumber
116         elif len(phonenumber) <= 7:
117                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
118         elif len(phonenumber) > 8 and phonenumber[0] == "1":
119                 prettynumber = "1 (%s)-%s-%s" %(phonenumber[1:4], phonenumber[4:7], phonenumber[7:]) 
120         elif len(phonenumber) > 7:
121                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
122         return prettynumber
123
124
125 class Dialpad(object):
126
127         __app_name__ = "gc_dialer"
128         __version__ = "0.7.0"
129
130
131         def __init__(self):
132                 self.phonenumber = ""
133                 self.prettynumber = ""
134                 self.areacode = "518"
135                 self.clipboard = gtk.clipboard_get()
136                 self.recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
137                 self.recentviewselection = None
138                 self.callbackNeedsSetup = True
139                 self.recenttime = 0.0
140
141                 for path in [ './gc_dialer.glade',
142                                 '../lib/gc_dialer.glade',
143                                 '/usr/local/lib/gc_dialer.glade' ]:
144                         if os.path.isfile(path):
145                                 #if gtk.glade is None:
146                                 #       self.wTree = gtk.Builder()
147                                 #       self.wTree.add_from_file(path)
148                                 #else:
149                                 self.wTree = gtk.glade.XML(path)
150                                 break
151                 else:
152                         self.ErrPopUp("Cannot find gc_dialer.glade")
153                         gtk.main_quit()
154                         return
155
156                 self.wTree.get_widget("about_title").set_label(self.wTree.get_widget("about_title").get_label()+"\nVersion "+Dialpad.__version__)
157
158                 #Get the buffer associated with the number display
159                 self.numberdisplay = self.wTree.get_widget("numberdisplay")
160                 self.setNumber("")
161                 self.notebook = self.wTree.get_widget("notebook")
162
163                 self.window = self.wTree.get_widget("Dialpad")
164
165                 global hildon
166                 self.app = None
167                 if hildon is not None and isinstance(self.window, gtk.Window):
168                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
169                         hildon = None
170                 elif hildon is not None:
171                         self.app = hildon.Program()
172                         self.window.set_title("Keypad")
173                         self.app.add_window(self.window)
174                         self.wTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
175                         self.wTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
176                         self.wTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
177                 else:
178                         warnings.warn("No Hildon", UserWarning, 2)
179
180                 if osso is not None:
181                         self.osso = osso.Context(__name__, Dialpad.__version__, False)
182                         device = osso.DeviceState(self.osso)
183                         device.set_device_state_callback(self.on_device_state_change, 0)
184                         if abook is not None and evobook is not None:
185                                 abook.init_with_name(__name__, self.osso)
186                                 self.ebook = evo.open_addressbook("default")
187                         else:
188                                 warnings.warn("No abook and No evolution address book support", UserWarning, 2)
189                 else:
190                         warnings.warn("No OSSO", UserWarning, 2)
191
192                 if self.window:
193                         self.window.connect("destroy", gtk.main_quit)
194                         self.window.show_all()
195
196                 callbackMapping = {
197                         # Process signals from buttons
198                         "on_digit_clicked"  : self.on_digit_clicked,
199                         "on_dial_clicked"    : self.on_dial_clicked,
200                         "on_loginbutton_clicked" : self.on_loginbutton_clicked,
201                         "on_loginclose_clicked" : self.on_loginclose_clicked,
202                         "on_clearcookies_clicked" : self.on_clearcookies_clicked,
203                 #       "on_callbackentry_changed" : self.on_callbackentry_changed,
204                         "on_notebook_switch_page" : self.on_notebook_switch_page,
205                         "on_recentview_row_activated" : self.on_recentview_row_activated,
206                         "on_back_clicked" : self.Backspace
207                 }
208                 self.wTree.signal_autoconnect(callbackMapping)
209                 self.wTree.get_widget("callbackcombo").get_child().connect("changed", self.on_callbackentry_changed)
210
211                 # Defer initalization of recent view
212                 self.gcd = GCDialer()
213
214                 self.attemptLogin(2)
215                 gobject.idle_add(self.init_grandcentral)
216                 #self.init_grandcentral()
217                 gobject.idle_add(self.init_recentview)
218
219                 #self.reduce_memory()
220
221         def init_grandcentral(self):
222                 """ deferred initalization of the grandcentral info """
223                 
224                 try:
225                         #self.attemptLogin(2)
226                         if self.gcd.isAuthed():
227                                 if self.gcd.getCallbackNumber() is None:
228                                         self.gcd.setSaneCallback()
229                 except:
230                         pass
231                 
232                 self.setAccountNumber()
233                 print "exit init_gc"
234                 return False
235
236         def init_recentview(self):
237                 """ deferred initalization of the recent view treeview """
238
239                 recentview = self.wTree.get_widget("recentview")
240                 recentview.set_model(self.recentmodel)
241                 textrenderer = gtk.CellRendererText()
242
243                 # Add the column to the treeview
244                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
245                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
246
247                 recentview.append_column(column)
248
249                 self.recentviewselection = recentview.get_selection()
250                 self.recentviewselection.set_mode(gtk.SELECTION_SINGLE)
251
252                 return False
253
254         def on_recentview_row_activated(self, treeview, path, view_column):
255                 model, itr = self.recentviewselection.get_selected()
256                 if not itr:
257                         return
258
259                 self.setNumber(self.recentmodel.get_value(itr, 0))
260                 self.notebook.set_current_page(0)
261                 self.recentviewselection.unselect_all()
262
263         def on_notebook_switch_page(self, notebook, page, page_num):
264                 if page_num == 1 and (time.time() - self.recenttime) > 300:
265                         gobject.idle_add(self.populate_recentview)
266                 elif page_num ==2 and self.callbackNeedsSetup:
267                         gobject.idle_add(self.setupCallbackCombo)
268                 if hildon:
269                         try:
270                                 self.window.set_title(self.notebook.get_tab_label(self.notebook.get_nth_page(page_num)).get_text())
271                         except:
272                                 self.window.set_title("")
273
274         def populate_recentview(self):
275                 print "Populating"
276                 self.recentmodel.clear()
277                 for item in self.gcd.get_recent():
278                         self.recentmodel.append(item)
279                 self.recenttime = time.time()
280
281                 return False
282
283         def on_clearcookies_clicked(self, data=None):
284                 self.gcd.reset()
285                 self.callbackNeedsSetup = True
286                 self.recenttime = 0.0
287                 self.recentmodel.clear()
288                 self.wTree.get_widget("callbackcombo").get_child().set_text("")
289         
290                 # re-run the inital grandcentral setup
291                 self.attemptLogin(2)
292                 gobject.idle_add(self.init_grandcentral)
293
294         def setupCallbackCombo(self):
295                 combobox = self.wTree.get_widget("callbackcombo")
296                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
297                 combobox.set_model(self.callbacklist)
298                 combobox.set_text_column(0)
299                 for number, description in self.gcd.getCallbackNumbers().iteritems():
300                         self.callbacklist.append([makepretty(number)] )
301
302                 self.wTree.get_widget("callbackcombo").get_child().set_text(makepretty(self.gcd.getCallbackNumber()))
303                 self.callbackNeedsSetup = False
304
305         def on_callbackentry_changed(self, data=None):
306                 text = makeugly(self.wTree.get_widget("callbackcombo").get_child().get_text())
307                 if self.gcd.validate(text) and text != self.gcd.getCallbackNumber():
308                         self.gcd.setCallbackNumber(text)
309                         #self.wTree.get_widget("callbackentry").set_text(self.wTree.get_object("callbackentry").get_text())
310                 #self.reduce_memory()
311
312         def attemptLogin(self, times = 1):
313                 #if self.isHildon:
314                 #       dialog = hildon.LoginDialog(self.window)
315                 #       dialog.set_message("Grandcentral Login")
316                 #else:
317                 dialog = self.wTree.get_widget("login_dialog")
318
319                 while (0 < times) and not self.gcd.isAuthed():
320                         if dialog.run() != gtk.RESPONSE_OK:
321                                 times = 0
322                                 continue
323
324                         #if self.isHildon:
325                         #       username = dialog.get_username()
326                         #       password = dialog.get_password()
327                         #else:
328                         username = self.wTree.get_widget("usernameentry").get_text()
329                         password = self.wTree.get_widget("passwordentry").get_text()
330                         self.wTree.get_widget("passwordentry").set_text("")
331                         print "Attempting login"
332                         self.gcd.login(username, password)
333                         print "hiding dialog"
334                         dialog.hide()
335                         times = times - 1
336
337                 #if self.isHildon:
338                 #       print "destroy dialog"
339                 #       dialog.destroy()
340
341                 return False
342
343         def ErrPopUp(self, msg):
344                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
345
346                 def close(dialog, response, editor):
347                         editor.about_dialog = None
348                         dialog.destroy()
349                 error_dialog.connect("response", close, self)
350                 self.error_dialog = error_dialog
351                 error_dialog.run()
352
353         def on_paste(self, data=None):
354                 contents = self.clipboard.wait_for_text()
355                 phoneNumber = re.sub('\D', '', contents)
356                 self.setNumber(phoneNumber)
357         
358         def on_loginbutton_clicked(self, data=None):
359                 self.wTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
360
361         def on_loginclose_clicked(self, data=None):
362                 sys.exit(0)
363
364         def on_dial_clicked(self, widget):
365                 self.attemptLogin(3)
366
367                 if not self.gcd.isAuthed() or self.gcd.getCallbackNumber() == "":
368                         self.ErrPopUp("Backend link with grandcentral is not working, please try again")
369                         return
370
371                 #if len(self.phonenumber) == 7:
372                 #       #add default area code
373                 #       self.phonenumber = self.areacode + self.phonenumber
374
375                 try:
376                         callSuccess = self.gcd.dial(self.phonenumber)
377                 except ValueError, e:
378                         self.gcd._msg = e.message
379                         callSuccess = False
380
381                 if not callSuccess:
382                         self.ErrPopUp(self.gcd._msg)
383                 else:
384                         self.setNumber("")
385
386                 self.recentmodel.clear()
387                 self.recenttime = 0.0
388                 #self.reduce_memory()
389         
390         def on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
391                 """
392                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
393                 For system_inactivity, we have no background tasks to pause
394
395                 @todo Might be useful to do something when going in offline mode or low memory
396                 @note Hildon specific
397                 """
398                 if shutdown or save_unsaved_data:
399                         pass
400
401                 if memory_low:
402                         self.gcd.clear_caches()
403                         re.purge()
404                         gc.collect()
405
406                 #if offline (how do I tell this? the message somehow?)
407                 #       disable the gui?
408                 #       disable clearing of caches and when they click dial, request to connect?
409
410         def setNumber(self, number):
411                 self.phonenumber = makeugly(number)
412                 self.prettynumber = makepretty(self.phonenumber)
413                 self.numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self.prettynumber ) )
414
415         def setAccountNumber(self):
416                 accountnumber = self.gcd.getAccountNumber()
417                 self.wTree.get_widget("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
418
419         def Backspace(self, widget):
420                 self.setNumber(self.phonenumber[:-1])
421
422         def on_digit_clicked(self, widget):
423                 self.setNumber(self.phonenumber + widget.get_name()[5])
424
425
426 def run_doctest():
427         failureCount, testCount = doctest.testmod()
428         if not failureCount:
429                 print "Tests Successful"
430                 sys.exit(0)
431         else:
432                 sys.exit(1)
433
434
435 def run_dialpad():
436         #gc.set_threshold(50, 3, 3)
437         gtk.gdk.threads_init()
438         title = 'Dialpad'
439         handle = Dialpad()
440         gtk.main()
441         sys.exit(1)
442
443
444 class DummyOptions(object):
445         def __init__(self):
446                 self.test = False
447
448
449 if __name__ == "__main__":
450         if hildon:
451                 gtk.set_application_name("Dialer")
452
453         try:
454                 parser = optparse.OptionParser()
455                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
456                 (options, args) = parser.parse_args()
457         except:
458                 args = []
459                 options = DummyOptions()
460
461         if options.test:
462                 run_doctest()
463         else:
464                 run_dialpad()