Under hildon set app name and change window title to be name of the tab we are on
[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 os
14 import re
15 import time
16 import threading
17 import contextlib
18 import gobject
19 import gtk
20 import gc
21 try:
22         import hildon
23 except:
24         pass
25
26 try:
27         import doctest
28         import optparse
29 except:
30         pass
31
32 from gcbackend import GCDialer
33
34 import socket
35 socket.setdefaulttimeout(5)
36
37 @contextlib.contextmanager
38 def gtk_critical_section():
39         #The API changed and I hope these are the right calls
40         gtk.gdk.threads_enter()
41         yield
42         gtk.gdk.threads_leave()
43
44
45 def makeugly(prettynumber):
46         """
47         function to take a phone number and strip out all non-numeric
48         characters
49
50         >>> makeugly("+012-(345)-678-90")
51         '01234567890'
52         """
53         uglynumber = re.sub('\D', '', prettynumber)
54         return uglynumber
55
56
57 def makepretty(phonenumber):
58         """
59         Function to take a phone number and return the pretty version
60          pretty numbers:
61                 if phonenumber begins with 0:
62                         ...-(...)-...-....
63                 if phonenumber begins with 1: ( for gizmo callback numbers )
64                         1 (...)-...-....
65                 if phonenumber is 13 digits:
66                         (...)-...-....
67                 if phonenumber is 10 digits:
68                         ...-....
69         >>> makepretty("12")
70         '12'
71         >>> makepretty("1234567")
72         '123-4567'
73         >>> makepretty("1234567890")
74         '(123)-456-7890'
75         >>> makepretty("01234567890")
76         '+012-(345)-678-90'
77         """
78         if phonenumber is None:
79                 return ""
80
81         if len(phonenumber) < 3:
82                 return phonenumber
83
84         if phonenumber[0] == "0":
85                 prettynumber = ""
86                 prettynumber += "+%s" % phonenumber[0:3]
87                 if 3 < len(phonenumber):
88                         prettynumber += "-(%s)" % phonenumber[3:6]
89                         if 6 < len(phonenumber):
90                                 prettynumber += "-%s" % phonenumber[6:9]
91                                 if 9 < len(phonenumber):
92                                         prettynumber += "-%s" % phonenumber[9:]
93                 return prettynumber
94         elif len(phonenumber) <= 7:
95                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
96         elif len(phonenumber) > 8 and phonenumber[0] == "1":
97                 prettynumber = "1 (%s)-%s-%s" %(phonenumber[1:4], phonenumber[4:7], phonenumber[7:]) 
98         elif len(phonenumber) > 7:
99                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
100         return prettynumber
101
102
103 class Dialpad(object):
104
105         def __init__(self):
106                 self.phonenumber = ""
107                 self.prettynumber = ""
108                 self.areacode = "518"
109                 self.clipboard = gtk.clipboard_get()
110                 self.wTree = gtk.Builder()
111                 self.recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
112                 self.recentviewselection = None
113                 self.callbackNeedsSetup = True
114                 self.recenttime = 0.0
115
116                 for path in [ './gc_dialer.xml',
117                                 '../lib/gc_dialer.xml',
118                                 '/usr/local/lib/gc_dialer.xml' ]:
119                         if os.path.isfile(path):
120                                 self.wTree.add_from_file(path)
121                                 break
122                 else:
123                         self.ErrPopUp("Cannot find gc_dialer.xml")
124                         gtk.main_quit()
125                         return
126
127
128                 #Get the buffer associated with the number display
129                 self.numberdisplay = self.wTree.get_object("numberdisplay")
130                 self.setNumber("")
131                 self.notebook = self.wTree.get_object("notebook")
132
133                 self.isHildon = False
134
135                 self.window = self.wTree.get_object("Dialpad")
136                 #if True:
137                 try:
138                         #self.osso = osso.Context("gc_dialer", "0.6.0", False)
139                         #device = osso.DeviceState(self.osso)
140                         #device.set_device_state_callback(self.on_device_state_change, None)
141                         #abook.init_with_name("gc_dialer", self.osso)
142                         #self.ebook = evo.open_addressbook("default")
143                         self.app = hildon.Program()
144                         self.window.set_title("Keypad")
145                         self.app.add_window(self.window)
146                         self.wTree.get_object("callbackentry").set_property('hildon-input-mode', (1 << 4))
147                         self.wTree.get_object("usernameentry").set_property('hildon-input-mode', 7)
148                         self.wTree.get_object("passwordentry").set_property('hildon-input-mode', 7)
149                         self.isHildon = True
150                 except:
151                         print "No hildon"
152
153                 if self.window:
154                         self.window.connect("destroy", gtk.main_quit)
155                         self.window.show_all()
156
157                 callbackMapping = {
158                         # Process signals from buttons
159                         "on_digit_clicked"  : self.on_digit_clicked,
160                         "on_dial_clicked"    : self.on_dial_clicked,
161                         "on_loginbutton_clicked" : self.on_loginbutton_clicked,
162                         "on_clearcookies_clicked" : self.on_clearcookies_clicked,
163                         "on_callbackentry_changed" : self.on_callbackentry_changed,
164                         "on_notebook_switch_page" : self.on_notebook_switch_page,
165                         "on_recentview_row_activated" : self.on_recentview_row_activated,
166                         "on_back_clicked" : self.Backspace
167                 }
168                 self.wTree.connect_signals(callbackMapping)
169
170                 # Defer initalization of recent view
171                 self.gcd = GCDialer()
172
173                 self.attemptLogin(2)
174                 gobject.idle_add(self.init_grandcentral)
175                 #self.init_grandcentral()
176                 gobject.idle_add(self.init_recentview)
177
178                 #self.reduce_memory()
179
180         def init_grandcentral(self):
181                 """ deferred initalization of the grandcentral info """
182                 
183                 try:
184                         #self.attemptLogin(2)
185                         if self.gcd.isAuthed():
186                                 if self.gcd.getCallbackNumber() is None:
187                                         self.gcd.setSaneCallback()
188                 except:
189                         pass
190                 
191                 self.setAccountNumber()
192                 print "exit init_gc"
193                 return False
194
195         def init_recentview(self):
196                 """ deferred initalization of the recent view treeview """
197
198                 recentview = self.wTree.get_object("recentview")
199                 recentview.set_model(self.recentmodel)
200                 textrenderer = gtk.CellRendererText()
201
202                 # Add the column to the treeview
203                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
204                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
205
206                 recentview.append_column(column)
207
208                 self.recentviewselection = recentview.get_selection()
209                 self.recentviewselection.set_mode(gtk.SELECTION_SINGLE)
210
211                 return False
212
213         def on_recentview_row_activated(self, treeview, path, view_column):
214                 model, itr = self.recentviewselection.get_selected()
215                 if not itr:
216                         return
217
218                 self.setNumber(self.recentmodel.get_value(itr, 0))
219                 self.notebook.set_current_page(0)
220                 self.recentviewselection.unselect_all()
221
222         def on_notebook_switch_page(self, notebook, page, page_num):
223                 if page_num == 1 and (time.time() - self.recenttime) > 300:
224                         gobject.idle_add(self.populate_recentview)
225                 elif page_num ==2 and self.callbackNeedsSetup:
226                         gobject.idle_add(self.setupCallbackCombo)
227                 if self.isHildon:
228                         try:
229                                 self.window.set_title(self.notebook.get_tab_label(self.notebook.get_nth_page(page_num)).get_text())
230                         except:
231                                 self.window.set_title("")
232
233         def populate_recentview(self):
234                 print "Populating"
235                 self.recentmodel.clear()
236                 for item in self.gcd.get_recent():
237                         self.recentmodel.append(item)
238                 self.recenttime = time.time()
239
240                 return False
241
242         def on_clearcookies_clicked(self, data=None):
243                 self.gcd.reset()
244                 self.callbackNeedsSetup = True
245                 self.recenttime = 0.0
246         
247                 # re-run the inital grandcentral setup
248                 self.attemptLogin(2)
249                 gobject.idle_add(self.init_grandcentral)
250
251         def setupCallbackCombo(self):
252                 combobox = self.wTree.get_object("callbackcombo")
253                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
254                 combobox.set_model(self.callbacklist)
255                 combobox.set_text_column(0)
256                 for number, description in self.gcd.getCallbackNumbers().iteritems():
257                         self.callbacklist.append([makepretty(number)] )
258
259                 self.wTree.get_object("callbackentry").set_text(makepretty(self.gcd.getCallbackNumber()))
260                 self.callbackNeedsSetup = False
261
262         def on_callbackentry_changed(self, data=None):
263                 text = makeugly(self.wTree.get_object("callbackentry").get_text())
264                 if self.gcd.validate(text) and text != self.gcd.getCallbackNumber():
265                         self.gcd.setCallbackNumber(text)
266                         self.wTree.get_object("callbackentry").set_text(self.wTree.get_object("callbackentry").get_text())
267                 #self.reduce_memory()
268
269         def attemptLogin(self, times = 1):
270                 #if self.isHildon:
271                 #       dialog = hildon.LoginDialog(self.window)
272                 #       dialog.set_message("Grandcentral Login")
273                 #else:
274                 dialog = self.wTree.get_object("login_dialog")
275
276                 while (0 < times) and not self.gcd.isAuthed():
277                         if dialog.run() != gtk.RESPONSE_OK:
278                                 times = 0
279                                 continue
280
281                         #if self.isHildon:
282                         #       username = dialog.get_username()
283                         #       password = dialog.get_password()
284                         #else:
285                         username = self.wTree.get_object("usernameentry").get_text()
286                         password = self.wTree.get_object("passwordentry").get_text()
287                         self.wTree.get_object("passwordentry").set_text("")
288                         print "Attempting login"
289                         self.gcd.login(username, password)
290                         print "hiding dialog"
291                         dialog.hide()
292                         times = times - 1
293
294                 #if self.isHildon:
295                 #       print "destroy dialog"
296                 #       dialog.destroy()
297
298                 return False
299
300         def ErrPopUp(self, msg):
301                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
302
303                 def close(dialog, response, editor):
304                         editor.about_dialog = None
305                         dialog.destroy()
306                 error_dialog.connect("response", close, self)
307                 self.error_dialog = error_dialog
308                 error_dialog.run()
309
310         def on_paste(self, data=None):
311                 contents = self.clipboard.wait_for_text()
312                 phoneNumber = re.sub('\D', '', contents)
313                 self.setNumber(phoneNumber)
314         
315         def on_loginbutton_clicked(self, data=None):
316                 self.wTree.get_object("login_dialog").response(gtk.RESPONSE_OK)
317
318         def on_dial_clicked(self, widget):
319                 self.attemptLogin(3)
320
321                 if not self.gcd.isAuthed() or self.gcd.getCallbackNumber() == "":
322                         self.ErrPopUp("Backend link with grandcentral is not working, please try again")
323                         return
324
325                 #if len(self.phonenumber) == 7:
326                 #       #add default area code
327                 #       self.phonenumber = self.areacode + self.phonenumber
328
329                 if self.gcd.dial(self.phonenumber) is False:
330                         self.ErrPopUp(self.gcd._msg)
331                 else:
332                         self.setNumber("")
333
334                 self.recentmodel.clear()
335                 self.recenttime = 0.0
336                 #self.reduce_memory()
337         
338         #def on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
339         #       """
340         #       @todo Might be useful to do something when going in offline mode or low memory
341         #       @note Hildon specific
342         #       """
343         #       pass
344
345         def setNumber(self, number):
346                 self.phonenumber = makeugly(number)
347                 self.prettynumber = makepretty(self.phonenumber)
348                 self.numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self.prettynumber ) )
349
350         def setAccountNumber(self):
351                 accountnumber = self.gcd.getAccountNumber()
352                 self.wTree.get_object("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
353
354         def Backspace(self, widget):
355                 self.setNumber(self.phonenumber[:-1])
356
357         def on_digit_clicked(self, widget):
358                 self.setNumber(self.phonenumber + widget.get_name()[5])
359
360
361 def run_doctest():
362         failureCount, testCount = doctest.testmod()
363         if not failureCount:
364                 print "Tests Successful"
365                 sys.exit(0)
366         else:
367                 sys.exit(1)
368
369
370 def run_dialpad():
371         gc.set_threshold(50, 3, 3)
372         gtk.gdk.threads_init()
373         title = 'Dialpad'
374         handle = Dialpad()
375         gtk.main()
376         sys.exit(1)
377
378
379 class DummyOptions(object):
380         def __init__(self):
381                 self.test = False
382
383
384 if __name__ == "__main__":
385         try:
386                 gtk.set_application_name("Dialer")
387         except:
388                 pass
389
390         try:
391                 parser = optparse.OptionParser()
392                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
393                 (options, args) = parser.parse_args()
394         except:
395                 args = []
396                 options = DummyOptions()
397
398         if options.test:
399                 run_doctest()
400         else:
401                 run_dialpad()