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