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