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