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