cba90e69a62049b655af5fa6362fcc4bc718bc9c
[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                 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_loginbutton_clicked": self._on_loginbutton_clicked,
207                         "on_loginclose_clicked": self._on_loginclose_clicked,
208
209                         "on_digit_clicked": self._on_digit_clicked,
210                         "on_dial_clicked": self._on_dial_clicked,
211                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
212                         "on_notebook_switch_page": self._on_notebook_switch_page,
213                         "on_recentview_row_activated": self._on_recentview_row_activated,
214                         "on_back_clicked": self._on_backspace
215                 }
216                 self._widgetTree.signal_autoconnect(callbackMapping)
217                 self._widgetTree.get_widget("callbackcombo").get_child().connect("changed", self._on_callbackentry_changed)
218
219                 self._gcBackend = GCDialer()
220
221                 self.attempt_login(2)
222                 gobject.idle_add(self._init_grandcentral)
223                 # Defer initalization of recent view
224                 gobject.idle_add(self._init_recent_view)
225
226         def _init_grandcentral(self):
227                 """ Deferred initalization of the grandcentral info """
228
229                 if self._gcBackend.is_authed():
230                         if self._gcBackend.get_callback_number() is None:
231                                 self._gcBackend.set_sane_callback()
232                         self.set_account_number()
233
234                 return False
235
236         def _init_recent_view(self):
237                 """ Deferred initalization of the recent view treeview """
238
239                 recentview = self._widgetTree.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 _setup_callback_combo(self):
255                 combobox = self._widgetTree.get_widget("callbackcombo")
256                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
257                 combobox.set_model(self.callbacklist)
258                 combobox.set_text_column(0)
259                 for number, description in self._gcBackend.get_callback_numbers().iteritems():
260                         self.callbacklist.append([make_pretty(number)] )
261
262                 self._widgetTree.get_widget("callbackcombo").get_child().set_text(make_pretty(self._gcBackend.get_callback_number()))
263                 self._callbackNeedsSetup = False
264
265         def populate_recentview(self):
266                 self._recentmodel.clear()
267                 for personsName, phoneNumber, date, action in self._gcBackend.get_recent():
268                         item = (phoneNumber, "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber))
269                         self._recentmodel.append(item)
270                 self._recenttime = time.time()
271
272                 return False
273
274         def attempt_login(self, times = 1):
275                 assert 0 < times, "That was pointless having 0 or less login attempts"
276                 dialog = self._widgetTree.get_widget("login_dialog")
277
278                 while (0 < times) and not self._gcBackend.is_authed():
279                         dialog.run()
280
281                         username = self._widgetTree.get_widget("usernameentry").get_text()
282                         password = self._widgetTree.get_widget("passwordentry").get_text()
283                         self._widgetTree.get_widget("passwordentry").set_text("")
284
285                         loggedIn = self._gcBackend.login(username, password)
286                         dialog.hide()
287                         if loggedIn:
288                                 return True
289                         times -= 1
290
291                 return False
292
293         def display_error_message(self, msg):
294                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
295
296                 def close(dialog, response, editor):
297                         editor.about_dialog = None
298                         dialog.destroy()
299                 error_dialog.connect("response", close, self)
300                 error_dialog.run()
301
302         def set_number(self, number):
303                 self._phonenumber = make_ugly(number)
304                 self._prettynumber = make_pretty(self._phonenumber)
305                 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self._prettynumber ) )
306
307         def set_account_number(self):
308                 accountnumber = self._gcBackend.get_account_number()
309                 self._widgetTree.get_widget("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
310
311         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
312                 """
313                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
314                 For system_inactivity, we have no background tasks to pause
315
316                 @note Hildon specific
317                 """
318                 if memory_low:
319                         self._gcBackend.clear_caches()
320                         re.purge()
321                         gc.collect()
322
323         def _on_connection_change(self, connection, event, magicIdentifier):
324                 """
325                 @note Hildon specific
326                 """
327                 status = event.get_status()
328                 error = event.get_error()
329                 iap_id = event.get_iap_id()
330                 bearer = event.get_bearer_type()
331
332                 if status == conic.STATUS_CONNECTED:
333                         self._window.set_sensitive(True)
334                         self._deviceIsOnline = True
335                 elif status == conic.STATUS_DISCONNECTED:
336                         self._window.set_sensitive(False)
337                         self._deviceIsOnline = False
338
339         def _on_loginbutton_clicked(self, data=None):
340                 self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
341
342         def _on_loginclose_clicked(self, data=None):
343                 sys.exit(0)
344
345         def _on_clearcookies_clicked(self, data=None):
346                 self._gcBackend.reset()
347                 self._callbackNeedsSetup = True
348                 self._recenttime = 0.0
349                 self._recentmodel.clear()
350                 self._widgetTree.get_widget("callbackcombo").get_child().set_text("")
351
352                 # re-run the inital grandcentral setup
353                 self.attempt_login(2)
354                 gobject.idle_add(self._init_grandcentral)
355
356         def _on_callbackentry_changed(self, data=None):
357                 """
358                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
359                 """
360                 text = make_ugly(self._widgetTree.get_widget("callbackcombo").get_child().get_text())
361                 if self._gcBackend.is_valid_syntax(text) and text != self._gcBackend.get_callback_number():
362                         self._gcBackend.set_callback_number(text)
363
364         def _on_recentview_row_activated(self, treeview, path, view_column):
365                 model, itr = self._recentviewselection.get_selected()
366                 if not itr:
367                         return
368
369                 self.set_number(self._recentmodel.get_value(itr, 0))
370                 self._notebook.set_current_page(0)
371                 self._recentviewselection.unselect_all()
372
373         def _on_notebook_switch_page(self, notebook, page, page_num):
374                 if page_num == 1 and (time.time() - self._recenttime) > 300:
375                         gobject.idle_add(self.populate_recentview)
376                 elif page_num ==2 and self._callbackNeedsSetup:
377                         gobject.idle_add(self._setup_callback_combo)
378
379                 if hildon:
380                         self._window.set_title(self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text())
381
382         def _on_dial_clicked(self, widget):
383                 """
384                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
385                 """
386                 loggedIn = self.attempt_login(2)
387                 if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
388                         self.display_error_message("Backend link with grandcentral is not working, please try again")
389                         return
390
391                 try:
392                         callSuccess = self._gcBackend.dial(self._phonenumber)
393                 except ValueError, e:
394                         self._gcBackend._msg = e.message
395                         callSuccess = False
396
397                 if not callSuccess:
398                         self.display_error_message(self._gcBackend._msg)
399                 else:
400                         self.set_number("")
401
402                 self._recentmodel.clear()
403                 self._recenttime = 0.0
404
405         def _on_paste(self, data=None):
406                 contents = self._clipboard.wait_for_text()
407                 phoneNumber = re.sub('\D', '', contents)
408                 self.set_number(phoneNumber)
409
410         def _on_digit_clicked(self, widget):
411                 self.set_number(self._phonenumber + widget.get_name()[5])
412
413         def _on_backspace(self, widget):
414                 self.set_number(self._phonenumber[:-1])
415
416
417 def run_doctest():
418         failureCount, testCount = doctest.testmod()
419         if not failureCount:
420                 print "Tests Successful"
421                 sys.exit(0)
422         else:
423                 sys.exit(1)
424
425
426 def run_dialpad():
427         gtk.gdk.threads_init()
428         title = 'Dialpad'
429         handle = Dialpad()
430         gtk.main()
431         sys.exit(0)
432
433
434 class DummyOptions(object):
435
436         def __init__(self):
437                 self.test = False
438
439
440 if __name__ == "__main__":
441         if hildon is not None:
442                 gtk.set_application_name("Dialer")
443
444         if optparse is not None:
445                 parser = optparse.OptionParser()
446                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
447                 (options, args) = parser.parse_args()
448         else:
449                 args = []
450                 options = DummyOptions()
451
452         if options.test:
453                 run_doctest()
454         else:
455                 run_dialpad()