a47a467ccee506b79101a9208f299bafdd9d0f21
[gc-dialer] / src / dialcentral / gc_dialer.py
1 #!/usr/bin/python2.5
2
3 # GC Dialer - Front end for Google's Grand Central service.
4 # Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
5
6 # This library is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU Lesser General Public
8 # License as published by the Free Software Foundation; either
9 # version 2.1 of the License, or (at your option) any later version.
10
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # Lesser General Public License for more details.
15
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with this library; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20
21 """
22 Grandcentral Dialer
23 """
24
25 import sys
26 import gc
27 import os
28 import threading
29 import time
30 import warnings
31
32 import gobject
33 import gtk
34 import gtk.glade
35
36 try:
37         import hildon
38 except ImportError:
39         hildon = None
40
41 import socket
42
43
44 gtk.gdk.threads_init()
45 #This changes the default, system wide, socket timeout so that a hung server will not completly
46 #hork the application
47 socket.setdefaulttimeout(5)
48
49
50 def make_ugly(prettynumber):
51         """
52         function to take a phone number and strip out all non-numeric
53         characters
54
55         >>> make_ugly("+012-(345)-678-90")
56         '01234567890'
57         """
58         import re
59         uglynumber = re.sub('\D', '', prettynumber)
60         return uglynumber
61
62
63 def make_pretty(phonenumber):
64         """
65         Function to take a phone number and return the pretty version
66         pretty numbers:
67                 if phonenumber begins with 0:
68                         ...-(...)-...-....
69                 if phonenumber begins with 1: ( for gizmo callback numbers )
70                         1 (...)-...-....
71                 if phonenumber is 13 digits:
72                         (...)-...-....
73                 if phonenumber is 10 digits:
74                         ...-....
75         >>> make_pretty("12")
76         '12'
77         >>> make_pretty("1234567")
78         '123-4567'
79         >>> make_pretty("2345678901")
80         '(234)-567-8901'
81         >>> make_pretty("12345678901")
82         '1 (234)-567-8901'
83         >>> make_pretty("01234567890")
84         '+012-(345)-678-90'
85         """
86         if phonenumber is None or phonenumber is "":
87                 return ""
88
89         if len(phonenumber) < 3:
90                 return phonenumber
91
92         if phonenumber[0] == "0":
93                 prettynumber = ""
94                 prettynumber += "+%s" % phonenumber[0:3]
95                 if 3 < len(phonenumber):
96                         prettynumber += "-(%s)" % phonenumber[3:6]
97                         if 6 < len(phonenumber):
98                                 prettynumber += "-%s" % phonenumber[6:9]
99                                 if 9 < len(phonenumber):
100                                         prettynumber += "-%s" % phonenumber[9:]
101                 return prettynumber
102         elif len(phonenumber) <= 7:
103                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
104         elif len(phonenumber) > 8 and phonenumber[0] == "1":
105                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
106         elif len(phonenumber) > 7:
107                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
108         return prettynumber
109
110
111 def make_idler(func):
112         """
113         Decorator that makes a generator-function into a function that will continue execution on next call
114         """
115         a = []
116
117         def decorated_func(*args, **kwds):
118                 if not a:
119                         a.append(func(*args, **kwds))
120                 try:
121                         a[0].next()
122                         return True
123                 except StopIteration:
124                         del a[:]
125                         return False
126         
127         decorated_func.__name__ = func.__name__
128         decorated_func.__doc__ = func.__doc__
129         decorated_func.__dict__.update(func.__dict__)
130
131         return decorated_func
132
133
134 class DummyAddressBook(object):
135         """
136         Minimal example of both an addressbook factory and an addressbook
137         """
138
139         def get_addressbooks(self):
140                 """
141                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
142                 """
143                 yield self, "", "None"
144         
145         def open_addressbook(self, bookId):
146                 return self
147
148         @staticmethod
149         def factory_short_name():
150                 return ""
151
152         @staticmethod
153         def factory_name():
154                 return ""
155
156         @staticmethod
157         def get_contacts():
158                 """
159                 @returns Iterable of (contact id, contact name)
160                 """
161                 return []
162
163         @staticmethod
164         def get_contact_details(contactId):
165                 """
166                 @returns Iterable of (Phone Type, Phone Number)
167                 """
168                 return []
169
170
171 class PhoneTypeSelector(object):
172
173         def __init__(self, widgetTree, gcBackend):
174                 self._gcBackend = gcBackend
175                 self._widgetTree = widgetTree
176                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
177
178                 self._selectButton = self._widgetTree.get_widget("select_button")
179                 self._selectButton.connect("clicked", self._on_phonetype_select)
180
181                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
182                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
183
184                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
185                 self._typeviewselection = None
186
187                 typeview = self._widgetTree.get_widget("phonetypes")
188                 typeview.connect("row-activated", self._on_phonetype_select)
189                 typeview.set_model(self._typemodel)
190                 textrenderer = gtk.CellRendererText()
191
192                 # Add the column to the treeview
193                 column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1)
194                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
195
196                 typeview.append_column(column)
197
198                 self._typeviewselection = typeview.get_selection()
199                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
200
201         def run(self, contactDetails):
202                 self._typemodel.clear()
203
204                 for phoneType, phoneNumber in contactDetails:
205                         self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
206
207                 userResponse = self._dialog.run()
208
209                 if userResponse == gtk.RESPONSE_OK:
210                         model, itr = self._typeviewselection.get_selected()
211                         if itr:
212                                 phoneNumber = self._typemodel.get_value(itr, 0)
213                 else:
214                         phoneNumber = ""
215
216                 self._typeviewselection.unselect_all()
217                 self._dialog.hide()
218                 return phoneNumber
219         
220         def _on_phonetype_select(self, *args):
221                 self._dialog.response(gtk.RESPONSE_OK)
222
223         def _on_phonetype_cancel(self, *args):
224                 self._dialog.response(gtk.RESPONSE_CANCEL)
225
226
227 class Dialpad(object):
228
229         __pretty_app_name__ = "DialCentral"
230         __app_name__ = "dialcentral"
231         __version__ = "0.8.0"
232         __app_magic__ = 0xdeadbeef
233
234         _glade_files = [
235                 './gc_dialer.glade',
236                 '../lib/gc_dialer.glade',
237                 '/usr/lib/dialcentral/gc_dialer.glade',
238         ]
239
240         def __init__(self):
241                 self._phonenumber = ""
242                 self._prettynumber = ""
243                 self._areacode = "518"
244
245                 self._clipboard = gtk.clipboard_get()
246
247                 self._deviceIsOnline = True
248                 self._callbackList = None
249
250                 self._recenttime = 0.0
251                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
252                 self._recentviewselection = None
253
254                 self._contactstime = 0.0
255                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
256                 self._contactsviewselection = None
257
258                 self._clearall_id = None
259                 
260                 for path in Dialpad._glade_files:
261                         if os.path.isfile(path):
262                                 self._widgetTree = gtk.glade.XML(path)
263                                 break
264                 else:
265                         self.display_error_message("Cannot find gc_dialer.glade")
266                         gtk.main_quit()
267                         return
268
269                 #Get the buffer associated with the number display
270                 self._numberdisplay = self._widgetTree.get_widget("numberdisplay")
271                 self.set_number("")
272                 self._notebook = self._widgetTree.get_widget("notebook")
273
274                 self._window = self._widgetTree.get_widget("Dialpad")
275
276                 global hildon
277                 self._app = None
278                 self._isFullScreen = False
279                 if hildon is not None and self._window is gtk.Window:
280                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
281                         hildon = None
282                 elif hildon is not None:
283                         self._app = hildon.Program()
284                         self._window = hildon.Window()
285                         self._widgetTree.get_widget("vbox1").reparent(self._window)
286                         self._app.add_window(self._window)
287                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
288                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
289                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
290
291                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
292                         menu = gtk.Menu()
293                         for child in gtkMenu.get_children():
294                                 child.reparent(menu)
295                         self._window.set_menu(menu)
296                         gtkMenu.destroy()
297
298                         self._window.connect("key-press-event", self._on_key_press)
299                         self._window.connect("window-state-event", self._on_window_state_change)
300                 else:
301                         warnings.warn("No Hildon", UserWarning, 2)
302
303                 if hildon is not None:
304                         self._window.set_title("Keypad")
305                 else:
306                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
307
308                 callbackMapping = {
309                         # Process signals from buttons
310                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
311                         "on_loginclose_clicked": self._on_loginclose_clicked,
312
313                         "on_dialpad_quit": self._on_close,
314                         "on_paste": self._on_paste,
315                         "on_clear_number": self._on_clear_number,
316
317                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
318                         "on_notebook_switch_page": self._on_notebook_switch_page,
319                         "on_recentview_row_activated": self._on_recentview_row_activated,
320                         "on_contactsview_row_activated" : self._on_contactsview_row_activated,
321
322                         "on_digit_clicked": self._on_digit_clicked,
323                         "on_back_clicked": self._on_backspace,
324                         "on_dial_clicked": self._on_dial_clicked,
325                         "on_back_pressed": self._on_back_pressed,
326                         "on_back_released": self._on_back_released,
327                         "on_addressbook_combo_changed": self._on_addressbook_combo_changed,
328                         "on_about_activate": self._on_about_activate,
329                 }
330                 self._widgetTree.signal_autoconnect(callbackMapping)
331                 self._widgetTree.get_widget("callbackcombo").get_child().connect("changed", self._on_callbackentry_changed)
332
333                 if self._window:
334                         self._window.connect("destroy", gtk.main_quit)
335                         self._window.show_all()
336
337                 self.set_account_number("")
338                 self._widgetTree.get_widget("dial").grab_default()
339                 self._widgetTree.get_widget("dial").grab_focus()
340
341                 threading.Thread(target=self._idle_setup).start()
342
343
344         def _idle_setup(self):
345                 """
346                 If something can be done after the UI loads, push it here so it's not blocking the UI
347                 """
348                 
349                 from gc_backend import GCDialer
350                 from evo_backend import EvolutionAddressBook
351
352                 self._gcBackend = GCDialer()
353
354                 try:
355                         import osso
356                 except ImportError:
357                         osso = None
358
359                 try:
360                         import conic
361                 except ImportError:
362                         conic = None
363
364
365                 self._osso = None
366                 if osso is not None:
367                         self._osso = osso.Context(Dialpad.__app_name__, Dialpad.__version__, False)
368                         device = osso.DeviceState(self._osso)
369                         device.set_device_state_callback(self._on_device_state_change, 0)
370                 else:
371                         warnings.warn("No OSSO", UserWarning, 2)
372
373                 self._connection = None
374                 if conic is not None:
375                         self._connection = conic.Connection()
376                         self._connection.connect("connection-event", self._on_connection_change, Dialpad.__app_magic__)
377                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
378                 else:
379                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
380
381
382                 self._addressBookFactories = [
383                         self._gcBackend,
384                         DummyAddressBook(),
385                         EvolutionAddressBook(),
386                 ]
387                 self._addressBook = None
388                 self.open_addressbook(*self.get_addressbooks().next()[0][0:2])
389         
390                 gtk.gdk.threads_enter()
391                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
392                 gtk.gdk.threads_leave()
393
394                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
395                         if factoryName and bookName:
396                                 entryName = "%s: %s" % (factoryName, bookName) 
397                         elif factoryName:
398                                 entryName = factoryName
399                         elif bookName:
400                                 entryName = bookName
401                         else:
402                                 entryName = "Bad name (%d)" % factoryId
403                         row = (str(factoryId), bookId, entryName)
404                         gtk.gdk.threads_enter()
405                         self._booksList.append(row)
406                         gtk.gdk.threads_leave()
407
408                 gtk.gdk.threads_enter()
409                 combobox = self._widgetTree.get_widget("addressbook_combo")
410                 combobox.set_model(self._booksList)
411                 cell = gtk.CellRendererText()
412                 combobox.pack_start(cell, True)
413                 combobox.add_attribute(cell, 'text', 2)
414                 combobox.set_active(0)
415                 gtk.gdk.threads_leave()
416
417                 self._phoneTypeSelector = PhoneTypeSelector(self._widgetTree, self._gcBackend)
418
419                 gtk.gdk.threads_enter()
420                 self._init_recent_view()
421                 self._init_contacts_view()
422                 gtk.gdk.threads_leave()
423
424                 #This is where the blocking can start
425                 if self._gcBackend.is_authed():
426                         gtk.gdk.threads_enter()
427                         self.set_account_number(self._gcBackend.get_account_number())
428                         self.populate_callback_combo()
429                         gtk.gdk.threads_leave()
430                 else:
431                         self.attempt_login(2)
432
433                 return False
434
435         def _init_recent_view(self):
436                 recentview = self._widgetTree.get_widget("recentview")
437                 recentview.set_model(self._recentmodel)
438                 textrenderer = gtk.CellRendererText()
439
440                 # Add the column to the treeview
441                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
442                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
443
444                 recentview.append_column(column)
445
446                 self._recentviewselection = recentview.get_selection()
447                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
448
449                 return False
450
451         def _init_contacts_view(self):
452                 contactsview = self._widgetTree.get_widget("contactsview")
453                 contactsview.set_model(self._contactsmodel)
454
455                 # Add the column to the treeview
456                 column = gtk.TreeViewColumn("Contact")
457
458                 textrenderer = gtk.CellRendererText()
459                 column.pack_start(textrenderer, expand=True)
460                 column.add_attribute(textrenderer, 'text', 1)
461
462                 textrenderer = gtk.CellRendererText()
463                 column.pack_start(textrenderer, expand=True)
464                 column.add_attribute(textrenderer, 'text', 4)
465
466                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
467                 column.set_sort_column_id(1)
468                 column.set_visible(True)
469                 contactsview.append_column(column)
470
471                 #textrenderer = gtk.CellRendererText()
472                 #column = gtk.TreeViewColumn("Location", textrenderer, text=2)
473                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
474                 #column.set_sort_column_id(2)
475                 #column.set_visible(True)
476                 #contactsview.append_column(column)
477
478                 #textrenderer = gtk.CellRendererText()
479                 #column = gtk.TreeViewColumn("Phone", textrenderer, text=3)
480                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
481                 #column.set_sort_column_id(3)
482                 #column.set_visible(True)
483                 #contactsview.append_column(column)
484
485                 self._contactsviewselection = contactsview.get_selection()
486                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
487
488                 return False
489
490         def populate_callback_combo(self):
491                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
492                 for number, description in self._gcBackend.get_callback_numbers().iteritems():
493                         self._callbackList.append((make_pretty(number),))
494
495                 combobox = self._widgetTree.get_widget("callbackcombo")
496                 combobox.set_model(self._callbackList)
497                 combobox.set_text_column(0)
498
499                 combobox.get_child().set_text(make_pretty(self._gcBackend.get_callback_number()))
500
501         def _idly_populate_recentview(self):
502                 self._recentmodel.clear()
503
504                 for personsName, phoneNumber, date, action in self._gcBackend.get_recent():
505                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
506                         item = (phoneNumber, description)
507                         gtk.gdk.threads_enter()
508                         self._recentmodel.append(item)
509                         gtk.gdk.threads_leave()
510
511                 self._recenttime = time.time()
512                 return False
513
514         @make_idler
515         def _idly_populate_contactsview(self):
516                 self._contactsmodel.clear()
517
518                 # completely disable updating the treeview while we populate the data
519                 contactsview = self._widgetTree.get_widget("contactsview")
520                 contactsview.freeze_child_notify()
521                 contactsview.set_model(None)
522
523                 contactType = (self._addressBook.factory_short_name(),)
524                 for contactId, contactName in self._addressBook.get_contacts():
525                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("",))
526                         yield
527
528                 # restart the treeview data rendering
529                 contactsview.set_model(self._contactsmodel)
530                 contactsview.thaw_child_notify()
531
532                 self._contactstime = time.time()
533
534         def attempt_login(self, numOfAttempts = 1):
535                 """
536                 @note Not meant to be called directly, but run as a seperate thread.
537                 """
538                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
539
540                 if not self._deviceIsOnline:
541                         warnings.warn("Attempted to login while device was offline", UserWarning, 2)
542                         return False
543
544                 if self._gcBackend.is_authed():
545                         return True
546                 
547                 for x in xrange(numOfAttempts):
548                         gtk.gdk.threads_enter()
549
550                         dialog = self._widgetTree.get_widget("login_dialog")
551                         dialog.set_transient_for(self._window)
552                         dialog.set_default_response(0)
553                         dialog.run()
554
555                         username = self._widgetTree.get_widget("usernameentry").get_text()
556                         password = self._widgetTree.get_widget("passwordentry").get_text()
557                         self._widgetTree.get_widget("passwordentry").set_text("")
558                         dialog.hide()
559                         gtk.gdk.threads_leave()
560                         loggedIn = self._gcBackend.login(username, password)
561                         if loggedIn:
562                                 gtk.gdk.threads_enter()
563                                 if self._gcBackend.get_callback_number() is None:
564                                         self._gcBackend.set_sane_callback()
565                                 self.populate_callback_combo()
566                                 self.set_account_number(self._gcBackend.get_account_number())
567                                 gtk.gdk.threads_leave()
568                                 return True
569
570         def display_error_message(self, msg):
571                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
572
573                 def close(dialog, response, editor):
574                         editor.about_dialog = None
575                         dialog.destroy()
576                 error_dialog.connect("response", close, self)
577                 error_dialog.run()
578
579         def get_addressbooks(self):
580                 """
581                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
582                 """
583                 for i, factory in enumerate(self._addressBookFactories):
584                         for bookFactory, bookId, bookName in factory.get_addressbooks():
585                                 yield (i, bookId), (factory.factory_name(), bookName)
586         
587         def open_addressbook(self, bookFactoryId, bookId):
588                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
589                 self._contactstime = 0
590                 gobject.idle_add(self._idly_populate_contactsview)
591
592         def set_number(self, number):
593                 """
594                 Set the callback phonenumber
595                 """
596                 self._phonenumber = make_ugly(number)
597                 self._prettynumber = make_pretty(self._phonenumber)
598                 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
599
600         def set_account_number(self, number):
601                 """
602                 Displays current account number
603                 """
604                 self._widgetTree.get_widget("gcnumber_display").set_label("<span size='23000' weight='bold'>%s</span>" % (number))
605
606         @staticmethod
607         def _on_close(*args, **kwds):
608                 gtk.main_quit()
609
610         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
611                 """
612                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
613                 For system_inactivity, we have no background tasks to pause
614
615                 @note Hildon specific
616                 """
617                 if memory_low:
618                         self._gcBackend.clear_caches()
619                         gc.collect()
620
621         def _on_connection_change(self, connection, event, magicIdentifier):
622                 """
623                 @note Hildon specific
624                 """
625                 import conic
626
627                 status = event.get_status()
628                 error = event.get_error()
629                 iap_id = event.get_iap_id()
630                 bearer = event.get_bearer_type()
631
632                 if status == conic.STATUS_CONNECTED:
633                         self._window.set_sensitive(True)
634                         self._deviceIsOnline = True
635                         threading.Thread(target=self.attempt_login, args=[2]).start()
636                 elif status == conic.STATUS_DISCONNECTED:
637                         self._window.set_sensitive(False)
638                         self._deviceIsOnline = False
639
640         def _on_window_state_change(self, widget, event, *args):
641                 """
642                 @note Hildon specific
643                 """
644                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
645                         self._isFullScreen = True
646                 else:
647                         self._isFullScreen = False
648         
649         def _on_key_press(self, widget, event, *args):
650                 """
651                 @note Hildon specific
652                 """
653                 if event.keyval == gtk.keysyms.F6:
654                         if self._isFullScreen:
655                                 self._window.unfullscreen()
656                         else:
657                                 self._window.fullscreen()
658
659         def _on_loginbutton_clicked(self, *args):
660                 self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
661
662         def _on_loginclose_clicked(self, *args):
663                 self._on_close()
664                 sys.exit(0)
665
666         def _on_clearcookies_clicked(self, *args):
667                 self._gcBackend.logout()
668                 self._recenttime = 0.0
669                 self._contactstime = 0.0
670                 self._recentmodel.clear()
671                 self._widgetTree.get_widget("callbackcombo").get_child().set_text("")
672                 self.set_account_number("")
673
674                 # re-run the inital grandcentral setup
675                 threading.Thread(target=self.attempt_login, args=[2]).start()
676                 #gobject.idle_add(self._idly_populate_callback_combo)
677
678         def _on_callbackentry_changed(self, *args):
679                 """
680                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
681                 """
682                 text = make_ugly(self._widgetTree.get_widget("callbackcombo").get_child().get_text())
683                 if not self._gcBackend.is_valid_syntax(text):
684                         warnings.warn("%s is not a valid callback number" % text, UserWarning, 2)
685                 elif text == self._gcBackend.get_callback_number():
686                         warnings.warn("Callback number already is %s" % self._gcBackend.get_callback_number(), UserWarning, 2)
687                 else:
688                         self._gcBackend.set_callback_number(text)
689
690         def _on_recentview_row_activated(self, treeview, path, view_column):
691                 model, itr = self._recentviewselection.get_selected()
692                 if not itr:
693                         return
694
695                 self.set_number(self._recentmodel.get_value(itr, 0))
696                 self._notebook.set_current_page(0)
697                 self._recentviewselection.unselect_all()
698
699         def _on_addressbook_combo_changed(self, *args, **kwds):
700                 combobox = self._widgetTree.get_widget("addressbook_combo")
701                 itr = combobox.get_active_iter()
702
703                 factoryId = int(self._booksList.get_value(itr, 0))
704                 bookId = self._booksList.get_value(itr, 1)
705                 self.open_addressbook(factoryId, bookId)
706
707         def _on_contactsview_row_activated(self, treeview, path, view_column):
708                 model, itr = self._contactsviewselection.get_selected()
709                 if not itr:
710                         return
711
712                 contactId = self._contactsmodel.get_value(itr, 3)
713                 contactDetails = self._addressBook.get_contact_details(contactId)
714                 contactDetails = [phoneNumber for phoneNumber in contactDetails]
715
716                 if len(contactDetails) == 0:
717                         phoneNumber = ""
718                 elif len(contactDetails) == 1:
719                         phoneNumber = contactDetails[0][1]
720                 else:
721                         phoneNumber = self._phoneTypeSelector.run(contactDetails)
722
723                 if 0 < len(phoneNumber):
724                         self.set_number(phoneNumber)
725                         self._notebook.set_current_page(0)
726
727                 self._contactsviewselection.unselect_all()
728
729         def _on_notebook_switch_page(self, notebook, page, page_num):
730                 if page_num == 1 and 300 < (time.time() - self._contactstime):
731                         threading.Thread(target=self._idly_populate_contactsview).start()
732                 elif page_num == 2 and 300 < (time.time() - self._recenttime):
733                         threading.Thread(target=self._idly_populate_recentview).start()
734                 #elif page_num == 3 and self._callbackNeedsSetup:
735                 #       gobject.idle_add(self._idly_populate_callback_combo)
736
737                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
738                 if hildon is not None:
739                         self._window.set_title(tabTitle)
740                 else:
741                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
742
743         def _on_dial_clicked(self, widget):
744                 """
745                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
746                 """
747                 loggedIn = self._gcBackend.is_authed()
748                 if not loggedIn:
749                         return
750                         #loggedIn = self.attempt_login(2)
751
752                 if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
753                         self.display_error_message("Backend link with grandcentral is not working, please try again")
754                         warnings.warn("Backend Status: Logged in? %s, Authenticated? %s, Callback=%s" % (loggedIn, self._gcBackend.is_authed(), self._gcBackend.get_callback_number()), UserWarning, 2)
755                         return
756
757                 try:
758                         callSuccess = self._gcBackend.dial(self._phonenumber)
759                 except ValueError, e:
760                         self._gcBackend._msg = e.message
761                         callSuccess = False
762
763                 if not callSuccess:
764                         self.display_error_message(self._gcBackend._msg)
765                 else:
766                         self.set_number("")
767
768                 self._recentmodel.clear()
769                 self._recenttime = 0.0
770
771         def _on_paste(self, *args):
772                 contents = self._clipboard.wait_for_text()
773                 phoneNumber = make_ugly(contents)
774                 self.set_number(phoneNumber)
775
776         def _on_clear_number(self, *args):
777                 self.set_number("")
778
779         def _on_digit_clicked(self, widget):
780                 self.set_number(self._phonenumber + widget.get_name()[5])
781
782         def _on_backspace(self, widget):
783                 self.set_number(self._phonenumber[:-1])
784
785         def _on_clearall(self):
786                 self.set_number("")
787                 return False
788
789         def _on_back_pressed(self, widget):
790                 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
791
792         def _on_back_released(self, widget):
793                 if self._clearall_id is not None:
794                         gobject.source_remove(self._clearall_id)
795                 self._clearall_id = None
796
797         def _on_about_activate(self, *args):
798                 dlg = gtk.AboutDialog()
799                 dlg.set_name(self.__pretty_app_name__)
800                 dlg.set_version(self.__version__)
801                 dlg.set_copyright("Copyright 2008 - LGPL")
802                 dlg.set_comments("Dialer is designed to interface with your Google Grandcentral account.  This application is not affiliated with Google or Grandcentral in any way")
803                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
804                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
805                 dlg.run()
806                 dlg.destroy()
807         
808
809 def run_doctest():
810         import doctest
811
812         failureCount, testCount = doctest.testmod()
813         if not failureCount:
814                 print "Tests Successful"
815                 sys.exit(0)
816         else:
817                 sys.exit(1)
818
819
820 def run_dialpad():
821         gtk.gdk.threads_init()
822         if hildon is not None:
823                 gtk.set_application_name(Dialpad.__pretty_app_name__)
824         title = 'Dialpad'
825         handle = Dialpad()
826         gtk.main()
827
828
829 class DummyOptions(object):
830
831         def __init__(self):
832                 self.test = False
833
834
835 if __name__ == "__main__":
836         if len(sys.argv) > 1:
837                 try:
838                         import optparse
839                 except ImportError:
840                         optparse = None
841
842                 if optparse is not None:
843                         parser = optparse.OptionParser()
844                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
845                         (commandOptions, commandArgs) = parser.parse_args()
846         else:
847                 commandOptions = DummyOptions()
848                 commandArgs = []
849
850         if commandOptions.test:
851                 run_doctest()
852         else:
853                 run_dialpad()