Some further minor cleanups
[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._on_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_recent_view)
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_recent_view(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 _setupCallbackCombo(self):
253                 combobox = self.wTree.get_widget("callbackcombo")
254                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
255                 combobox.set_model(self.callbacklist)
256                 combobox.set_text_column(0)
257                 for number, description in self.gcd.getCallbackNumbers().iteritems():
258                         self.callbacklist.append([makepretty(number)] )
259
260                 self.wTree.get_widget("callbackcombo").get_child().set_text(makepretty(self.gcd.getCallbackNumber()))
261                 self.callbackNeedsSetup = False
262
263         def populate_recentview(self):
264                 print "Populating"
265                 self.recentmodel.clear()
266                 for item in self.gcd.get_recent():
267                         self.recentmodel.append(item)
268                 self.recenttime = time.time()
269
270                 return False
271
272         def attemptLogin(self, times = 1):
273                 dialog = self.wTree.get_widget("login_dialog")
274
275                 while (0 < times) and not self.gcd.isAuthed():
276                         if dialog.run() != gtk.RESPONSE_OK:
277                                 times = 0
278                                 continue
279
280                         username = self.wTree.get_widget("usernameentry").get_text()
281                         password = self.wTree.get_widget("passwordentry").get_text()
282                         self.wTree.get_widget("passwordentry").set_text("")
283                         print "Attempting login"
284                         self.gcd.login(username, password)
285                         print "hiding dialog"
286                         dialog.hide()
287                         times = times - 1
288
289                 return False
290
291         def ErrPopUp(self, msg):
292                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
293
294                 def close(dialog, response, editor):
295                         editor.about_dialog = None
296                         dialog.destroy()
297                 error_dialog.connect("response", close, self)
298                 error_dialog.run()
299
300         def setNumber(self, number):
301                 self.phonenumber = makeugly(number)
302                 self.prettynumber = makepretty(self.phonenumber)
303                 self.numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self.prettynumber ) )
304
305         def setAccountNumber(self):
306                 accountnumber = self.gcd.getAccountNumber()
307                 self.wTree.get_widget("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
308
309         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
310                 """
311                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
312                 For system_inactivity, we have no background tasks to pause
313
314                 @note Hildon specific
315                 """
316                 if memory_low:
317                         self.gcd.clear_caches()
318                         re.purge()
319                         gc.collect()
320
321         def _on_connection_change(self, connection, event, magicIdentifier):
322                 """
323                 @note Hildon specific
324                 """
325                 status = event.get_status()
326                 error = event.get_error()
327                 iap_id = event.get_iap_id()
328                 bearer = event.get_bearer_type()
329
330                 if status == conic.STATUS_CONNECTED:
331                         self.window.set_sensitive(True)
332                 elif status == conic.STATUS_DISCONNECTED:
333                         self.window.set_sensitive(False)
334
335         def _on_loginbutton_clicked(self, data=None):
336                 self.wTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
337
338         def _on_loginclose_clicked(self, data=None):
339                 sys.exit(0)
340
341         def _on_clearcookies_clicked(self, data=None):
342                 self.gcd.reset()
343                 self.callbackNeedsSetup = True
344                 self.recenttime = 0.0
345                 self.recentmodel.clear()
346                 self.wTree.get_widget("callbackcombo").get_child().set_text("")
347         
348                 # re-run the inital grandcentral setup
349                 self.attemptLogin(2)
350                 gobject.idle_add(self._init_grandcentral)
351
352         def _on_callbackentry_changed(self, data=None):
353                 text = makeugly(self.wTree.get_widget("callbackcombo").get_child().get_text())
354                 if self.gcd.validate(text) and text != self.gcd.getCallbackNumber():
355                         self.gcd.setCallbackNumber(text)
356
357         def _on_recentview_row_activated(self, treeview, path, view_column):
358                 model, itr = self.recentviewselection.get_selected()
359                 if not itr:
360                         return
361
362                 self.setNumber(self.recentmodel.get_value(itr, 0))
363                 self.notebook.set_current_page(0)
364                 self.recentviewselection.unselect_all()
365
366         def _on_notebook_switch_page(self, notebook, page, page_num):
367                 if page_num == 1 and (time.time() - self.recenttime) > 300:
368                         gobject.idle_add(self.populate_recentview)
369                 elif page_num ==2 and self.callbackNeedsSetup:
370                         gobject.idle_add(self._setupCallbackCombo)
371                 if hildon:
372                         try:
373                                 self.window.set_title(self.notebook.get_tab_label(self.notebook.get_nth_page(page_num)).get_text())
374                         except:
375                                 self.window.set_title("")
376
377         def _on_dial_clicked(self, widget):
378                 self.attemptLogin(3)
379
380                 if not self.gcd.isAuthed() or self.gcd.getCallbackNumber() == "":
381                         self.ErrPopUp("Backend link with grandcentral is not working, please try again")
382                         return
383
384                 try:
385                         callSuccess = self.gcd.dial(self.phonenumber)
386                 except ValueError, e:
387                         self.gcd._msg = e.message
388                         callSuccess = False
389
390                 if not callSuccess:
391                         self.ErrPopUp(self.gcd._msg)
392                 else:
393                         self.setNumber("")
394
395                 self.recentmodel.clear()
396                 self.recenttime = 0.0
397         
398         def _on_paste(self, data=None):
399                 contents = self.clipboard.wait_for_text()
400                 phoneNumber = re.sub('\D', '', contents)
401                 self.setNumber(phoneNumber)
402         
403         def _on_digit_clicked(self, widget):
404                 self.setNumber(self.phonenumber + widget.get_name()[5])
405
406         def _on_backspace(self, widget):
407                 self.setNumber(self.phonenumber[:-1])
408
409
410 def run_doctest():
411         failureCount, testCount = doctest.testmod()
412         if not failureCount:
413                 print "Tests Successful"
414                 sys.exit(0)
415         else:
416                 sys.exit(1)
417
418
419 def run_dialpad():
420         gtk.gdk.threads_init()
421         title = 'Dialpad'
422         handle = Dialpad()
423         gtk.main()
424         sys.exit(0)
425
426
427 class DummyOptions(object):
428         def __init__(self):
429                 self.test = False
430
431
432 if __name__ == "__main__":
433         if hildon:
434                 gtk.set_application_name("Dialer")
435
436         try:
437                 parser = optparse.OptionParser()
438                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
439                 (options, args) = parser.parse_args()
440         except:
441                 args = []
442                 options = DummyOptions()
443
444         if options.test:
445                 run_doctest()
446         else:
447                 run_dialpad()