* Made note of what widget callbacks could use some idle-action deferment for respon...
[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
137                 self._clipboard = gtk.clipboard_get()
138
139                 self._deviceIsOnline = True
140                 self._callbackNeedsSetup = True
141                 self._recenttime = 0.0
142                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
143                 self._recentviewselection = None
144
145                 for path in Dialpad._glade_files:
146                         if os.path.isfile(path):
147                                 self._widgetTree = gtk.glade.XML(path)
148                                 break
149                 else:
150                         self.ErrPopUp("Cannot find gc_dialer.glade")
151                         gtk.main_quit()
152                         return
153
154                 self._widgetTree.get_widget("about_title").set_label(self._widgetTree.get_widget("about_title").get_label()+"\nVersion "+Dialpad.__version__)
155
156                 #Get the buffer associated with the number display
157                 self._numberdisplay = self._widgetTree.get_widget("numberdisplay")
158                 self.setNumber("")
159                 self._notebook = self._widgetTree.get_widget("notebook")
160
161                 self._window = self._widgetTree.get_widget("Dialpad")
162
163                 global hildon
164                 self._app = None
165                 if hildon is not None and isinstance(self._window, gtk.Window):
166                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
167                         hildon = None
168                 elif hildon is not None:
169                         self._app = hildon.Program()
170                         self._window.set_title("Keypad")
171                         self._app.add_window(self._window)
172                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
173                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
174                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
175                 else:
176                         warnings.warn("No Hildon", UserWarning, 2)
177
178                 self._osso = None
179                 self._ebook = None
180                 if osso is not None:
181                         self._osso = osso.Context(Dialpad.__app_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(Dialpad.__app_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                 self._connection = None
193                 if conic is not None:
194                         self._connection = conic.Connection()
195                         self._connection.connect("connection-event", self._on_connection_change, Dialpad.__app_magic__)
196                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
197                 else:
198                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
199
200                 if self._window:
201                         self._window.connect("destroy", gtk.main_quit)
202                         self._window.show_all()
203
204                 callbackMapping = {
205                         # Process signals from buttons
206                         "on_digit_clicked"  : self._on_digit_clicked,
207                         "on_dial_clicked"    : self._on_dial_clicked,
208                         "on_loginbutton_clicked" : self._on_loginbutton_clicked,
209                         "on_loginclose_clicked" : self._on_loginclose_clicked,
210                         "on_clearcookies_clicked" : self._on_clearcookies_clicked,
211                         "on_notebook_switch_page" : self._on_notebook_switch_page,
212                         "on_recentview_row_activated" : self._on_recentview_row_activated,
213                         "on_back_clicked" : self._on_backspace
214                 }
215                 self._widgetTree.signal_autoconnect(callbackMapping)
216                 self._widgetTree.get_widget("callbackcombo").get_child().connect("changed", self._on_callbackentry_changed)
217
218                 # Defer initalization of recent view
219                 self._gcBackend = GCDialer()
220
221                 self.attemptLogin(2) #@todo maybe we should disable the GUI and defer this?
222                 gobject.idle_add(self._init_grandcentral)
223                 gobject.idle_add(self._init_recent_view)
224
225         def _init_grandcentral(self):
226                 """ deferred initalization of the grandcentral info """
227                 
228                 try:
229                         if self._gcBackend.isAuthed():
230                                 if self._gcBackend.getCallbackNumber() is None:
231                                         self._gcBackend.setSaneCallback()
232                 except:
233                         pass
234                 
235                 self.setAccountNumber()
236                 print "exit init_gc"
237                 return False
238
239         def _init_recent_view(self):
240                 """ deferred initalization of the recent view treeview """
241
242                 recentview = self._widgetTree.get_widget("recentview")
243                 recentview.set_model(self._recentmodel)
244                 textrenderer = gtk.CellRendererText()
245
246                 # Add the column to the treeview
247                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
248                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
249
250                 recentview.append_column(column)
251
252                 self._recentviewselection = recentview.get_selection()
253                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
254
255                 return False
256
257         def _setupCallbackCombo(self):
258                 combobox = self._widgetTree.get_widget("callbackcombo")
259                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
260                 combobox.set_model(self.callbacklist)
261                 combobox.set_text_column(0)
262                 for number, description in self._gcBackend.getCallbackNumbers().iteritems():
263                         self.callbacklist.append([makepretty(number)] )
264
265                 self._widgetTree.get_widget("callbackcombo").get_child().set_text(makepretty(self._gcBackend.getCallbackNumber()))
266                 self._callbackNeedsSetup = False
267
268         def populate_recentview(self):
269                 print "Populating"
270                 self._recentmodel.clear()
271                 for item in self._gcBackend.get_recent():
272                         self._recentmodel.append(item)
273                 self._recenttime = time.time()
274
275                 return False
276
277         def attemptLogin(self, times = 1):
278                 dialog = self._widgetTree.get_widget("login_dialog")
279
280                 while (0 < times) and not self._gcBackend.isAuthed():
281                         if dialog.run() != gtk.RESPONSE_OK:
282                                 times = 0
283                                 continue
284
285                         username = self._widgetTree.get_widget("usernameentry").get_text()
286                         password = self._widgetTree.get_widget("passwordentry").get_text()
287                         self._widgetTree.get_widget("passwordentry").set_text("")
288                         print "Attempting login"
289                         self._gcBackend.login(username, password)
290                         print "hiding dialog"
291                         dialog.hide()
292                         times = times - 1
293
294                 return False
295
296         def ErrPopUp(self, msg):
297                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
298
299                 def close(dialog, response, editor):
300                         editor.about_dialog = None
301                         dialog.destroy()
302                 error_dialog.connect("response", close, self)
303                 error_dialog.run()
304
305         def setNumber(self, number):
306                 self._phonenumber = makeugly(number)
307                 self._prettynumber = makepretty(self._phonenumber)
308                 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self._prettynumber ) )
309
310         def setAccountNumber(self):
311                 accountnumber = self._gcBackend.getAccountNumber()
312                 self._widgetTree.get_widget("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
313
314         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
315                 """
316                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
317                 For system_inactivity, we have no background tasks to pause
318
319                 @note Hildon specific
320                 """
321                 if memory_low:
322                         self._gcBackend.clear_caches()
323                         re.purge()
324                         gc.collect()
325
326         def _on_connection_change(self, connection, event, magicIdentifier):
327                 """
328                 @note Hildon specific
329                 """
330                 status = event.get_status()
331                 error = event.get_error()
332                 iap_id = event.get_iap_id()
333                 bearer = event.get_bearer_type()
334
335                 if status == conic.STATUS_CONNECTED:
336                         self._window.set_sensitive(True)
337                         self._deviceIsOnline = True
338                 elif status == conic.STATUS_DISCONNECTED:
339                         self._window.set_sensitive(False)
340                         self._deviceIsOnline = False
341
342         def _on_loginbutton_clicked(self, data=None):
343                 self._widgetTree.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_clearcookies_clicked(self, data=None):
349                 self._gcBackend.reset()
350                 self._callbackNeedsSetup = True
351                 self._recenttime = 0.0
352                 self._recentmodel.clear()
353                 self._widgetTree.get_widget("callbackcombo").get_child().set_text("")
354         
355                 # re-run the inital grandcentral setup
356                 self.attemptLogin(2)
357                 gobject.idle_add(self._init_grandcentral)
358
359         def _on_callbackentry_changed(self, data=None):
360                 """
361                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
362                 """
363                 text = makeugly(self._widgetTree.get_widget("callbackcombo").get_child().get_text())
364                 if self._gcBackend.validate(text) and text != self._gcBackend.getCallbackNumber():
365                         self._gcBackend.setCallbackNumber(text)
366
367         def _on_recentview_row_activated(self, treeview, path, view_column):
368                 model, itr = self._recentviewselection.get_selected()
369                 if not itr:
370                         return
371
372                 self.setNumber(self._recentmodel.get_value(itr, 0))
373                 self._notebook.set_current_page(0)
374                 self._recentviewselection.unselect_all()
375
376         def _on_notebook_switch_page(self, notebook, page, page_num):
377                 if page_num == 1 and (time.time() - self._recenttime) > 300:
378                         gobject.idle_add(self.populate_recentview)
379                 elif page_num ==2 and self._callbackNeedsSetup:
380                         gobject.idle_add(self._setupCallbackCombo)
381
382                 if hildon:
383                         try:
384                                 self._window.set_title(self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text())
385                         except:
386                                 self._window.set_title("")
387
388         def _on_dial_clicked(self, widget):
389                 """
390                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
391                 """
392                 self.attemptLogin(3)
393
394                 if not self._gcBackend.isAuthed() or self._gcBackend.getCallbackNumber() == "":
395                         self.ErrPopUp("Backend link with grandcentral is not working, please try again")
396                         return
397
398                 try:
399                         callSuccess = self._gcBackend.dial(self._phonenumber)
400                 except ValueError, e:
401                         self._gcBackend._msg = e.message
402                         callSuccess = False
403
404                 if not callSuccess:
405                         self.ErrPopUp(self._gcBackend._msg)
406                 else:
407                         self.setNumber("")
408
409                 self._recentmodel.clear()
410                 self._recenttime = 0.0
411         
412         def _on_paste(self, data=None):
413                 contents = self._clipboard.wait_for_text()
414                 phoneNumber = re.sub('\D', '', contents)
415                 self.setNumber(phoneNumber)
416         
417         def _on_digit_clicked(self, widget):
418                 self.setNumber(self._phonenumber + widget.get_name()[5])
419
420         def _on_backspace(self, widget):
421                 self.setNumber(self._phonenumber[:-1])
422
423
424 def run_doctest():
425         failureCount, testCount = doctest.testmod()
426         if not failureCount:
427                 print "Tests Successful"
428                 sys.exit(0)
429         else:
430                 sys.exit(1)
431
432
433 def run_dialpad():
434         gtk.gdk.threads_init()
435         title = 'Dialpad'
436         handle = Dialpad()
437         gtk.main()
438         sys.exit(0)
439
440
441 class DummyOptions(object):
442         def __init__(self):
443                 self.test = False
444
445
446 if __name__ == "__main__":
447         if hildon:
448                 gtk.set_application_name("Dialer")
449
450         try:
451                 parser = optparse.OptionParser()
452                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
453                 (options, args) = parser.parse_args()
454         except:
455                 args = []
456                 options = DummyOptions()
457
458         if options.test:
459                 run_doctest()
460         else:
461                 run_dialpad()