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