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