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