Adding a makefile
[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 make_ugly(prettynumber):
61         """
62         function to take a phone number and strip out all non-numeric
63         characters
64
65         >>> make_ugly("+012-(345)-678-90")
66         '01234567890'
67         """
68         uglynumber = re.sub('\D', '', prettynumber)
69         return uglynumber
70
71
72 def make_pretty(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         >>> make_pretty("12")
85         '12'
86         >>> make_pretty("1234567")
87         '123-4567'
88         >>> make_pretty("2345678901")
89         '(234)-567-8901'
90         >>> make_pretty("12345678901")
91         '1 (234)-567-8901'
92         >>> make_pretty("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.display_error_message("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.set_number("")
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                 self._isFullScreen = False
166                 if hildon is not None and isinstance(self._window, gtk.Window):
167                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
168                         hildon = None
169                 elif hildon is not None:
170                         self._app = hildon.Program()
171                         self._window.set_title("Keypad")
172                         self._app.add_window(self._window)
173                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
174                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
175                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
176
177                         gtkMenu = self._widgetTree.get_widget("menubar1")
178                         menu = gtk.Menu()
179                         for child in gtkMenu.get_children():
180                                 child.reparent(menu)
181                         self._window.set_menu(menu)
182                         gtkMenu.destroy()
183
184                         self._window.connect("key-press-event", self._on_key_press)
185                         self._window.connect("window-state-event", self._on_window_state_change)
186                 else:
187                         warnings.warn("No Hildon", UserWarning, 2)
188
189                 self._osso = None
190                 self._ebook = None
191                 if osso is not None:
192                         self._osso = osso.Context(Dialpad.__app_name__, Dialpad.__version__, False)
193                         device = osso.DeviceState(self._osso)
194                         device.set_device_state_callback(self._on_device_state_change, 0)
195                         if abook is not None and evobook is not None:
196                                 abook.init_with_name(Dialpad.__app_name__, self._osso)
197                                 self._ebook = evobook.open_addressbook("default")
198                         else:
199                                 warnings.warn("No abook and No evolution address book support", UserWarning, 2)
200                 else:
201                         warnings.warn("No OSSO", UserWarning, 2)
202
203                 self._connection = None
204                 if conic is not None:
205                         self._connection = conic.Connection()
206                         self._connection.connect("connection-event", self._on_connection_change, Dialpad.__app_magic__)
207                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
208                 else:
209                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
210
211                 callbackMapping = {
212                         # Process signals from buttons
213                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
214                         "on_loginclose_clicked": self._on_loginclose_clicked,
215
216                         "on_dialpad_quit": (lambda data: gtk.main_quit()),
217                         "on_paste": self._on_paste,
218                         "on_clear_number": self._on_clear_number,
219
220                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
221                         "on_notebook_switch_page": self._on_notebook_switch_page,
222                         "on_recentview_row_activated": self._on_recentview_row_activated,
223
224                         "on_digit_clicked": self._on_digit_clicked,
225                         "on_back_clicked": self._on_backspace,
226                         "on_dial_clicked": self._on_dial_clicked,
227                 }
228                 self._widgetTree.signal_autoconnect(callbackMapping)
229
230                 if self._window:
231                         self._window.connect("destroy", gtk.main_quit)
232                         self._window.show_all()
233
234                 self._gcBackend = GCDialer()
235
236                 self.attempt_login(2)
237                 gobject.idle_add(self._init_grandcentral)
238                 # Defer initalization of recent view
239                 gobject.idle_add(self._init_recent_view)
240
241         def _init_grandcentral(self):
242                 """ Deferred initalization of the grandcentral info """
243
244                 if self._gcBackend.is_authed():
245                         if self._gcBackend.get_callback_number() is None:
246                                 self._gcBackend.set_sane_callback()
247                         self.set_account_number()
248
249                 return False
250
251         def _init_recent_view(self):
252                 """ Deferred initalization of the recent view treeview """
253
254                 recentview = self._widgetTree.get_widget("recentview")
255                 recentview.set_model(self._recentmodel)
256                 textrenderer = gtk.CellRendererText()
257
258                 # Add the column to the treeview
259                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
260                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
261
262                 recentview.append_column(column)
263
264                 self._recentviewselection = recentview.get_selection()
265                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
266
267                 return False
268
269         def _setup_callback_combo(self):
270                 combobox = self._widgetTree.get_widget("callbackcombo")
271                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
272                 combobox.set_model(self.callbacklist)
273                 combobox.set_text_column(0)
274                 for number, description in self._gcBackend.get_callback_numbers().iteritems():
275                         self.callbacklist.append([make_pretty(number)] )
276
277                 self._widgetTree.get_widget("callbackcombo").get_child().set_text(make_pretty(self._gcBackend.get_callback_number()))
278                 self._callbackNeedsSetup = False
279
280         def populate_recentview(self):
281                 self._recentmodel.clear()
282                 for personsName, phoneNumber, date, action in self._gcBackend.get_recent():
283                         item = (phoneNumber, "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber))
284                         self._recentmodel.append(item)
285                 self._recenttime = time.time()
286
287                 return False
288
289         def attempt_login(self, times = 1):
290                 assert 0 < times, "That was pointless having 0 or less login attempts"
291                 dialog = self._widgetTree.get_widget("login_dialog")
292
293                 while (0 < times) and not self._gcBackend.is_authed():
294                         dialog.run()
295
296                         username = self._widgetTree.get_widget("usernameentry").get_text()
297                         password = self._widgetTree.get_widget("passwordentry").get_text()
298                         self._widgetTree.get_widget("passwordentry").set_text("")
299
300                         loggedIn = self._gcBackend.login(username, password)
301                         dialog.hide()
302                         if loggedIn:
303                                 return True
304                         times -= 1
305
306                 return False
307
308         def display_error_message(self, msg):
309                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
310
311                 def close(dialog, response, editor):
312                         editor.about_dialog = None
313                         dialog.destroy()
314                 error_dialog.connect("response", close, self)
315                 error_dialog.run()
316
317         def set_number(self, number):
318                 self._phonenumber = make_ugly(number)
319                 self._prettynumber = make_pretty(self._phonenumber)
320                 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self._prettynumber ) )
321
322         def set_account_number(self):
323                 accountnumber = self._gcBackend.get_account_number()
324                 self._widgetTree.get_widget("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
325
326         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
327                 """
328                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
329                 For system_inactivity, we have no background tasks to pause
330
331                 @note Hildon specific
332                 """
333                 if memory_low:
334                         self._gcBackend.clear_caches()
335                         re.purge()
336                         gc.collect()
337
338         def _on_connection_change(self, connection, event, magicIdentifier):
339                 """
340                 @note Hildon specific
341                 """
342                 status = event.get_status()
343                 error = event.get_error()
344                 iap_id = event.get_iap_id()
345                 bearer = event.get_bearer_type()
346
347                 if status == conic.STATUS_CONNECTED:
348                         self._window.set_sensitive(True)
349                         self._deviceIsOnline = True
350                 elif status == conic.STATUS_DISCONNECTED:
351                         self._window.set_sensitive(False)
352                         self._deviceIsOnline = False
353
354         def _on_window_state_change(self, widget, event, *args):
355                 """
356                 @note Hildon specific
357                 """
358                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
359                         self._isFullScreen = True
360                 else:
361                         self._isFullScreen = False
362
363         def _on_key_press(self, widget, event, *args):
364                 """
365                 @note Hildon specific
366                 """
367                 if event.keyval == gtk.keysyms.F6:
368                         if self._isFullScreen:
369                                 self._window.unfullscreen()
370                         else:
371                                 self._window.fullscreen()
372
373         def _on_loginbutton_clicked(self, data=None):
374                 self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
375
376         def _on_loginclose_clicked(self, data=None):
377                 sys.exit(0)
378
379         def _on_clearcookies_clicked(self, data=None):
380                 self._gcBackend.reset()
381                 self._callbackNeedsSetup = True
382                 self._recenttime = 0.0
383                 self._recentmodel.clear()
384                 self._widgetTree.get_widget("callbackcombo").get_child().set_text("")
385
386                 # re-run the inital grandcentral setup
387                 self.attempt_login(2)
388                 gobject.idle_add(self._init_grandcentral)
389
390         def _on_callbackentry_changed(self, data=None):
391                 """
392                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
393                 """
394                 text = make_ugly(self._widgetTree.get_widget("callbackcombo").get_child().get_text())
395                 if self._gcBackend.is_valid_syntax(text) and text != self._gcBackend.get_callback_number():
396                         self._gcBackend.set_callback_number(text)
397
398         def _on_recentview_row_activated(self, treeview, path, view_column):
399                 model, itr = self._recentviewselection.get_selected()
400                 if not itr:
401                         return
402
403                 self.set_number(self._recentmodel.get_value(itr, 0))
404                 self._notebook.set_current_page(0)
405                 self._recentviewselection.unselect_all()
406
407         def _on_notebook_switch_page(self, notebook, page, page_num):
408                 if page_num == 1 and (time.time() - self._recenttime) > 300:
409                         gobject.idle_add(self.populate_recentview)
410                 elif page_num ==2 and self._callbackNeedsSetup:
411                         gobject.idle_add(self._setup_callback_combo)
412
413                 if hildon:
414                         self._window.set_title(self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text())
415
416         def _on_dial_clicked(self, widget):
417                 """
418                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
419                 """
420                 loggedIn = self.attempt_login(2)
421                 if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
422                         self.display_error_message("Backend link with grandcentral is not working, please try again")
423                         return
424
425                 try:
426                         callSuccess = self._gcBackend.dial(self._phonenumber)
427                 except ValueError, e:
428                         self._gcBackend._msg = e.message
429                         callSuccess = False
430
431                 if not callSuccess:
432                         self.display_error_message(self._gcBackend._msg)
433                 else:
434                         self.set_number("")
435
436                 self._recentmodel.clear()
437                 self._recenttime = 0.0
438
439         def _on_paste(self, data=None):
440                 contents = self._clipboard.wait_for_text()
441                 phoneNumber = re.sub('\D', '', contents)
442                 self.set_number(phoneNumber)
443
444         def _on_clear_number(self, data=None):
445                 self.set_number("")
446
447         def _on_digit_clicked(self, widget):
448                 self.set_number(self._phonenumber + widget.get_name()[5])
449
450         def _on_backspace(self, widget):
451                 self.set_number(self._phonenumber[:-1])
452
453
454 def run_doctest():
455         failureCount, testCount = doctest.testmod()
456         if not failureCount:
457                 print "Tests Successful"
458                 sys.exit(0)
459         else:
460                 sys.exit(1)
461
462
463 def run_dialpad():
464         gtk.gdk.threads_init()
465         title = 'Dialpad'
466         handle = Dialpad()
467         gtk.main()
468         sys.exit(0)
469
470
471 class DummyOptions(object):
472
473         def __init__(self):
474                 self.test = False
475
476
477 if __name__ == "__main__":
478         if hildon is not None:
479                 gtk.set_application_name("Dialer")
480
481         if optparse is not None:
482                 parser = optparse.OptionParser()
483                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
484                 (options, args) = parser.parse_args()
485         else:
486                 args = []
487                 options = DummyOptions()
488
489         if options.test:
490                 run_doctest()
491         else:
492                 run_dialpad()