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