cf00b5609c1fc63e4c8e4ae43777eb0015e8e189
[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
176         def clear_caches(self):
177                 for addressBook in self.__addressbooks:
178                         addressBook.clear_caches()
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         def __init__(self, widgetTree):
289                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
290                 self._phonenumber = ""
291                 self._prettynumber = ""
292                 self._clearall_id = None
293
294                 callbackMapping = {
295                         "on_dial_clicked": self._on_dial_clicked,
296                         "on_digit_clicked": self._on_digit_clicked,
297                         "on_clear_number": self._on_clear_number,
298                         "on_back_clicked": self._on_backspace,
299                         "on_back_pressed": self._on_back_pressed,
300                         "on_back_released": self._on_back_released,
301                 }
302                 widgetTree.signal_autoconnect(callbackMapping)
303                 widgetTree.get_widget("dial").grab_default()
304                 widgetTree.get_widget("dial").grab_focus()
305
306         def get_number(self):
307                 return self._phonenumber
308
309         def set_number(self, number):
310                 """
311                 Set the callback phonenumber
312                 """
313                 self._phonenumber = make_ugly(number)
314                 self._prettynumber = make_pretty(self._phonenumber)
315                 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
316
317         def clear(self):
318                 self.set_number("")
319
320         def _on_dial_clicked(self, widget):
321                 self.dial(self.get_number())
322
323         def _on_clear_number(self, *args):
324                 self.clear()
325
326         def _on_digit_clicked(self, widget):
327                 self.set_number(self._phonenumber + widget.get_name()[-1])
328
329         def _on_backspace(self, widget):
330                 self.set_number(self._phonenumber[:-1])
331
332         def _on_clearall(self):
333                 self.clear()
334                 return False
335
336         def _on_back_pressed(self, widget):
337                 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
338
339         def _on_back_released(self, widget):
340                 if self._clearall_id is not None:
341                         gobject.source_remove(self._clearall_id)
342                 self._clearall_id = None
343
344
345 class AccountInfo(object):
346
347         def __init__(self, widgetTree, backend = None):
348                 self._backend = backend
349
350                 self._callbackList = None
351                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
352                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
353                 if hildon is not None:
354                         self._callbackCombo.get_child().set_property('hildon-input-mode', (1 << 4))
355
356                 callbackMapping = {
357                 }
358                 widgetTree.signal_autoconnect(callbackMapping)
359                 self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
360
361                 self.set_account_number("")
362
363         def set_backend(self, backend):
364                 self._backend = backend
365
366         def get_selected_callback_number(self):
367                 return make_ugly(self._callbackCombo.get_child().get_text())
368
369         def set_account_number(self, number):
370                 """
371                 Displays current account number
372                 """
373                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
374
375         def update(self):
376                 if self._backend is None:
377                         return
378
379                 self.populate_callback_combo()
380                 self.set_account_number(self._backend.get_account_number())
381
382         def clear(self):
383                 self._callbackCombo.get_child().set_text("")
384                 self.set_account_number("")
385
386         def populate_callback_combo(self):
387                 if self._backend is None:
388                         return
389
390                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
391                 for number, description in self._backend.get_callback_numbers().iteritems():
392                         self._callbackList.append((make_pretty(number),))
393
394                 self._callbackCombo.set_model(self._callbackList)
395                 self._callbackCombo.set_text_column(0)
396                 self._callbackCombo.get_child().set_text(make_pretty(self._backend.get_callback_number()))
397
398         def _on_callbackentry_changed(self, *args):
399                 """
400                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
401                 """
402                 if self._backend is None:
403                         return
404
405                 text = self.get_selected_callback_number()
406                 if not self._backend.is_valid_syntax(text):
407                         warnings.warn("%s is not a valid callback number" % text, UserWarning, 2)
408                 elif text == self._backend.get_callback_number():
409                         warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
410                 else:
411                         self._backend.set_callback_number(text)
412
413
414 class RecentCallsView(object):
415
416         def __init__(self, widgetTree, backend = None):
417                 self._backend = backend
418                 self._recenttime = 0.0
419                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
420                 self._recentview = widgetTree.get_widget("recentview")
421                 self._recentviewselection = None
422
423                 callbackMapping = {
424                         "on_recentview_row_activated": self._on_recentview_row_activated,
425                 }
426                 widgetTree.signal_autoconnect(callbackMapping)
427
428                 self._init_recent_view()
429
430         def set_backend(self, backend):
431                 self._backend = backend
432
433         def update(self):
434                 if (time.time() - self._recenttime) < 300:
435                         return
436                 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
437                 backgroundPopulate.setDaemon(True)
438                 backgroundPopulate.start()
439
440         def clear(self):
441                 self._recenttime = 0.0
442                 self._recentmodel.clear()
443
444         def _init_recent_view(self):
445                 self._recentview.set_model(self._recentmodel)
446                 textrenderer = gtk.CellRendererText()
447
448                 # Add the column to the treeview
449                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
450                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
451
452                 self._recentview.append_column(column)
453
454                 self._recentviewselection = self._recentview.get_selection()
455                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
456
457         def _idly_populate_recentview(self):
458                 self._recenttime = time.time()
459                 self._recentmodel.clear()
460
461                 for personsName, phoneNumber, date, action in self._backend.get_recent():
462                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
463                         item = (phoneNumber, description)
464                         gtk.gdk.threads_enter()
465                         self._recentmodel.append(item)
466                         gtk.gdk.threads_leave()
467
468                 return False
469
470         def _on_recentview_row_activated(self, treeview, path, view_column):
471                 model, itr = self._recentviewselection.get_selected()
472                 if not itr:
473                         return
474
475                 self.number_selected(self._recentmodel.get_value(itr, 0))
476                 self._recentviewselection.unselect_all()
477
478
479 class Dialcentral(object):
480
481         __pretty_app_name__ = "DialCentral"
482         __app_name__ = "dialcentral"
483         __version__ = "0.8.4"
484         __app_magic__ = 0xdeadbeef
485
486         _glade_files = [
487                 '/usr/lib/dialcentral/gc_dialer.glade',
488                 os.path.join(os.path.dirname(__file__), "gc_dialer.glade"),
489                 os.path.join(os.path.dirname(__file__), "../lib/gc_dialer.glade"),
490         ]
491
492         def __init__(self):
493                 self._gcBackend = None
494                 self._booksList = None
495                 self._addressBook = None
496                 self._addressBookFactories = None
497                 self._phoneTypeSelector = None
498
499                 self._clipboard = gtk.clipboard_get()
500
501                 self._deviceIsOnline = True
502
503                 self._contactstime = 0.0
504                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
505                 self._contactsviewselection = None
506
507                 for path in Dialcentral._glade_files:
508                         if os.path.isfile(path):
509                                 self._widgetTree = gtk.glade.XML(path)
510                                 break
511                 else:
512                         self.display_error_message("Cannot find gc_dialer.glade")
513                         gtk.main_quit()
514                         return
515
516                 #Get the buffer associated with the number display
517                 self._dialpad = Dialpad(self._widgetTree)
518                 self._dialpad.set_number("")
519                 self._dialpad.dial = self._on_dial_clicked
520                 self._accountView = AccountInfo(self._widgetTree)
521                 self._recentView = RecentCallsView(self._widgetTree)
522                 self._recentView.number_selected = self._on_number_selected
523
524                 self._window = self._widgetTree.get_widget("Dialpad")
525                 self._notebook = self._widgetTree.get_widget("notebook")
526
527                 global hildon
528                 self._app = None
529                 self._isFullScreen = False
530                 if hildon is not None and self._window is gtk.Window:
531                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
532                         hildon = None
533                 elif hildon is not None:
534                         self._app = hildon.Program()
535                         self._window = hildon.Window()
536                         self._widgetTree.get_widget("vbox1").reparent(self._window)
537                         self._app.add_window(self._window)
538                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
539                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
540                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
541                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
542
543                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
544                         menu = gtk.Menu()
545                         for child in gtkMenu.get_children():
546                                 child.reparent(menu)
547                         self._window.set_menu(menu)
548                         gtkMenu.destroy()
549
550                         self._window.connect("key-press-event", self._on_key_press)
551                         self._window.connect("window-state-event", self._on_window_state_change)
552                 else:
553                         warnings.warn("No Hildon", UserWarning, 2)
554
555                 if hildon is not None:
556                         self._window.set_title("Keypad")
557                 else:
558                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
559
560                 callbackMapping = {
561                         # Process signals from buttons
562                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
563                         "on_loginclose_clicked": self._on_loginclose_clicked,
564
565                         "on_dialpad_quit": self._on_close,
566                         "on_paste": self._on_paste,
567
568                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
569                         "on_notebook_switch_page": self._on_notebook_switch_page,
570                         "on_contactsview_row_activated" : self._on_contactsview_row_activated,
571
572                         "on_addressbook_combo_changed": self._on_addressbook_combo_changed,
573                         "on_about_activate": self._on_about_activate,
574                 }
575                 self._widgetTree.signal_autoconnect(callbackMapping)
576
577                 if self._window:
578                         self._window.connect("destroy", gtk.main_quit)
579                         self._window.show_all()
580
581                 backgroundSetup = threading.Thread(target=self._idle_setup)
582                 backgroundSetup.setDaemon(True)
583                 backgroundSetup.start()
584
585
586         def _idle_setup(self):
587                 """
588                 If something can be done after the UI loads, push it here so it's not blocking the UI
589                 """
590
591                 import gc_backend
592                 import evo_backend
593                 # import gmail_backend
594                 # import maemo_backend
595
596                 self._gcBackend = gc_backend.GCDialer()
597                 self._accountView.set_backend(self._gcBackend)
598                 self._recentView.set_backend(self._gcBackend)
599
600                 try:
601                         import osso
602                 except ImportError:
603                         osso = None
604
605                 try:
606                         import conic
607                 except ImportError:
608                         conic = None
609
610
611                 self._osso = None
612                 if osso is not None:
613                         self._osso = osso.Context(Dialcentral.__app_name__, Dialcentral.__version__, False)
614                         device = osso.DeviceState(self._osso)
615                         device.set_device_state_callback(self._on_device_state_change, 0)
616                 else:
617                         warnings.warn("No OSSO", UserWarning, 2)
618
619                 self._connection = None
620                 if conic is not None:
621                         self._connection = conic.Connection()
622                         self._connection.connect("connection-event", self._on_connection_change, Dialcentral.__app_magic__)
623                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
624                 else:
625                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
626
627
628                 addressBooks = [
629                         self._gcBackend,
630                         evo_backend.EvolutionAddressBook(),
631                         DummyAddressBook(),
632                 ]
633                 mergedBook = MergedAddressBook(addressBooks, MergedAddressBook.basic_lastname_sorter)
634                 self._addressBookFactories = list(addressBooks)
635                 self._addressBookFactories.insert(0, mergedBook)
636                 self._addressBook = None
637                 self.open_addressbook(*self.get_addressbooks().next()[0][0:2])
638
639                 gtk.gdk.threads_enter()
640                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
641                 gtk.gdk.threads_leave()
642
643                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
644                         if factoryName and bookName:
645                                 entryName = "%s: %s" % (factoryName, bookName)
646                         elif factoryName:
647                                 entryName = factoryName
648                         elif bookName:
649                                 entryName = bookName
650                         else:
651                                 entryName = "Bad name (%d)" % factoryId
652                         row = (str(factoryId), bookId, entryName)
653                         gtk.gdk.threads_enter()
654                         self._booksList.append(row)
655                         gtk.gdk.threads_leave()
656
657                 gtk.gdk.threads_enter()
658                 combobox = self._widgetTree.get_widget("addressbook_combo")
659                 combobox.set_model(self._booksList)
660                 cell = gtk.CellRendererText()
661                 combobox.pack_start(cell, True)
662                 combobox.add_attribute(cell, 'text', 2)
663                 combobox.set_active(0)
664                 gtk.gdk.threads_leave()
665
666                 self._phoneTypeSelector = PhoneTypeSelector(self._widgetTree, self._gcBackend)
667
668                 gtk.gdk.threads_enter()
669                 self._init_contacts_view()
670                 gtk.gdk.threads_leave()
671
672                 #This is where the blocking can start
673                 if self._gcBackend.is_authed():
674                         gtk.gdk.threads_enter()
675                         self._accountView.update()
676                         gtk.gdk.threads_leave()
677                 else:
678                         self.attempt_login(2)
679
680                 return False
681
682         def _init_contacts_view(self):
683                 contactsview = self._widgetTree.get_widget("contactsview")
684                 contactsview.set_model(self._contactsmodel)
685
686                 # Add the column to the treeview
687                 column = gtk.TreeViewColumn("Contact")
688
689                 #displayContactSource = False
690                 displayContactSource = True
691                 if displayContactSource:
692                         textrenderer = gtk.CellRendererText()
693                         column.pack_start(textrenderer, expand=False)
694                         column.add_attribute(textrenderer, 'text', 0)
695
696                 textrenderer = gtk.CellRendererText()
697                 column.pack_start(textrenderer, expand=True)
698                 column.add_attribute(textrenderer, 'text', 1)
699
700                 textrenderer = gtk.CellRendererText()
701                 column.pack_start(textrenderer, expand=True)
702                 column.add_attribute(textrenderer, 'text', 4)
703
704                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
705                 column.set_sort_column_id(1)
706                 column.set_visible(True)
707                 contactsview.append_column(column)
708
709                 self._contactsviewselection = contactsview.get_selection()
710                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
711
712                 return False
713
714         def _idly_populate_contactsview(self):
715                 #@todo Add a lock so only one code path can be in here at a time
716                 self._contactstime = time.time()
717                 self._contactsmodel.clear()
718
719                 # completely disable updating the treeview while we populate the data
720                 contactsview = self._widgetTree.get_widget("contactsview")
721                 contactsview.freeze_child_notify()
722                 contactsview.set_model(None)
723
724                 addressBook = self._addressBook
725                 for contactId, contactName in addressBook.get_contacts():
726                         contactType = (addressBook.contact_source_short_name(contactId),)
727                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("",))
728
729                 # restart the treeview data rendering
730                 contactsview.set_model(self._contactsmodel)
731                 contactsview.thaw_child_notify()
732                 return False
733
734         def attempt_login(self, numOfAttempts = 1):
735                 """
736                 @todo Handle user notification better like attempting to login and failed login
737
738                 @note Not meant to be called directly, but run as a seperate thread.
739                 """
740                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
741
742                 if not self._deviceIsOnline:
743                         warnings.warn("Attempted to login while device was offline", UserWarning, 2)
744                         return False
745
746                 if self._gcBackend.is_authed():
747                         return True
748
749                 for x in xrange(numOfAttempts):
750                         gtk.gdk.threads_enter()
751
752                         dialog = self._widgetTree.get_widget("login_dialog")
753                         dialog.set_transient_for(self._window)
754                         dialog.set_default_response(0)
755                         dialog.run()
756
757                         username = self._widgetTree.get_widget("usernameentry").get_text()
758                         password = self._widgetTree.get_widget("passwordentry").get_text()
759                         self._widgetTree.get_widget("passwordentry").set_text("")
760                         dialog.hide()
761                         gtk.gdk.threads_leave()
762                         loggedIn = self._gcBackend.login(username, password)
763                         if loggedIn:
764                                 gtk.gdk.threads_enter()
765                                 if self._gcBackend.get_callback_number() is None:
766                                         self._gcBackend.set_sane_callback()
767                                 self._accountView.update()
768                                 gtk.gdk.threads_leave()
769                                 return True
770
771                 return False
772
773         def display_error_message(self, msg):
774                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
775
776                 def close(dialog, response, editor):
777                         editor.about_dialog = None
778                         dialog.destroy()
779                 error_dialog.connect("response", close, self)
780                 error_dialog.run()
781
782         def get_addressbooks(self):
783                 """
784                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
785                 """
786                 for i, factory in enumerate(self._addressBookFactories):
787                         for bookFactory, bookId, bookName in factory.get_addressbooks():
788                                 yield (i, bookId), (factory.factory_name(), bookName)
789
790         def open_addressbook(self, bookFactoryId, bookId):
791                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
792                 self._contactstime = 0
793                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
794                 backgroundPopulate.setDaemon(True)
795                 backgroundPopulate.start()
796
797         @staticmethod
798         def _on_close(*args, **kwds):
799                 gtk.main_quit()
800
801         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
802                 """
803                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
804                 For system_inactivity, we have no background tasks to pause
805
806                 @note Hildon specific
807                 """
808                 if memory_low:
809                         self._gcBackend.clear_caches()
810                         for factory in self._addressBookFactories:
811                                 factory.clear_caches()
812                         self._addressBook.clear_caches()
813                         gc.collect()
814
815         def _on_connection_change(self, connection, event, magicIdentifier):
816                 """
817                 @note Hildon specific
818                 """
819                 import conic
820
821                 status = event.get_status()
822                 error = event.get_error()
823                 iap_id = event.get_iap_id()
824                 bearer = event.get_bearer_type()
825
826                 if status == conic.STATUS_CONNECTED:
827                         self._window.set_sensitive(True)
828                         self._deviceIsOnline = True
829                         backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
830                         backgroundLogin.setDaemon(True)
831                         backgroundLogin.start()
832                 elif status == conic.STATUS_DISCONNECTED:
833                         self._window.set_sensitive(False)
834                         self._deviceIsOnline = False
835
836         def _on_window_state_change(self, widget, event, *args):
837                 """
838                 @note Hildon specific
839                 """
840                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
841                         self._isFullScreen = True
842                 else:
843                         self._isFullScreen = False
844
845         def _on_key_press(self, widget, event, *args):
846                 """
847                 @note Hildon specific
848                 """
849                 if event.keyval == gtk.keysyms.F6:
850                         if self._isFullScreen:
851                                 self._window.unfullscreen()
852                         else:
853                                 self._window.fullscreen()
854
855         def _on_loginbutton_clicked(self, *args):
856                 self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
857
858         def _on_loginclose_clicked(self, *args):
859                 self._on_close()
860                 sys.exit(0)
861
862         def _on_clearcookies_clicked(self, *args):
863                 self._gcBackend.logout()
864                 self._contactstime = 0.0
865                 self._accountView.clear()
866                 self._recentView.clear()
867
868                 # re-run the inital grandcentral setup
869                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
870                 backgroundLogin.setDaemon(True)
871                 backgroundLogin.start()
872
873         def _on_addressbook_combo_changed(self, *args, **kwds):
874                 combobox = self._widgetTree.get_widget("addressbook_combo")
875                 itr = combobox.get_active_iter()
876
877                 factoryId = int(self._booksList.get_value(itr, 0))
878                 bookId = self._booksList.get_value(itr, 1)
879                 self.open_addressbook(factoryId, bookId)
880
881         def _on_contactsview_row_activated(self, treeview, path, view_column):
882                 model, itr = self._contactsviewselection.get_selected()
883                 if not itr:
884                         return
885
886                 contactId = self._contactsmodel.get_value(itr, 3)
887                 contactDetails = self._addressBook.get_contact_details(contactId)
888                 contactDetails = [phoneNumber for phoneNumber in contactDetails]
889
890                 if len(contactDetails) == 0:
891                         phoneNumber = ""
892                 elif len(contactDetails) == 1:
893                         phoneNumber = contactDetails[0][1]
894                 else:
895                         phoneNumber = self._phoneTypeSelector.run(contactDetails)
896
897                 if 0 < len(phoneNumber):
898                         self._dialpad.set_number(phoneNumber)
899                         self._notebook.set_current_page(0)
900
901                 self._contactsviewselection.unselect_all()
902
903         def _on_notebook_switch_page(self, notebook, page, page_num):
904                 if page_num == 1:
905                         if 300 < (time.time() - self._contactstime):
906                                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
907                                 backgroundPopulate.setDaemon(True)
908                                 backgroundPopulate.start()
909                 elif page_num == 3:
910                         self._recentView.update()
911                 #elif page_num == 2:
912                 #       self._callbackNeedsSetup::
913                 #               gobject.idle_add(self._idly_populate_callback_combo)
914
915                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
916                 if hildon is not None:
917                         self._window.set_title(tabTitle)
918                 else:
919                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
920
921         def _on_number_selected(self, number):
922                 self._dialpad.set_number(number)
923                 self._notebook.set_current_page(0)
924
925         def _on_dial_clicked(self, number):
926                 """
927                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
928                 """
929                 loggedIn = self._gcBackend.is_authed()
930                 if not loggedIn:
931                         return
932                         #loggedIn = self.attempt_login(2)
933
934                 if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
935                         self.display_error_message("Backend link with grandcentral is not working, please try again")
936                         warnings.warn("Backend Status: Logged in? %s, Authenticated? %s, Callback=%s" % (loggedIn, self._gcBackend.is_authed(), self._gcBackend.get_callback_number()), UserWarning, 2)
937                         return
938
939                 try:
940                         callSuccess = self._gcBackend.dial(number)
941                 except ValueError, e:
942                         self._gcBackend._msg = e.message
943                         callSuccess = False
944
945                 if not callSuccess:
946                         self.display_error_message(self._gcBackend._msg)
947                 else:
948                         self._dialpad.clear()
949
950                 self._recentView.clear()
951
952         def _on_paste(self, *args):
953                 contents = self._clipboard.wait_for_text()
954                 phoneNumber = make_ugly(contents)
955                 self._dialpad.set_number(phoneNumber)
956
957         def _on_about_activate(self, *args):
958                 dlg = gtk.AboutDialog()
959                 dlg.set_name(self.__pretty_app_name__)
960                 dlg.set_version(self.__version__)
961                 dlg.set_copyright("Copyright 2008 - LGPL")
962                 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")
963                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
964                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
965                 dlg.run()
966                 dlg.destroy()
967
968
969 def run_doctest():
970         import doctest
971
972         failureCount, testCount = doctest.testmod()
973         if not failureCount:
974                 print "Tests Successful"
975                 sys.exit(0)
976         else:
977                 sys.exit(1)
978
979
980 def run_dialpad():
981         gtk.gdk.threads_init()
982         if hildon is not None:
983                 gtk.set_application_name(Dialcentral.__pretty_app_name__)
984         handle = Dialcentral()
985         gtk.main()
986
987
988 class DummyOptions(object):
989
990         def __init__(self):
991                 self.test = False
992
993
994 if __name__ == "__main__":
995         if len(sys.argv) > 1:
996                 try:
997                         import optparse
998                 except ImportError:
999                         optparse = None
1000
1001                 if optparse is not None:
1002                         parser = optparse.OptionParser()
1003                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
1004                         (commandOptions, commandArgs) = parser.parse_args()
1005         else:
1006                 commandOptions = DummyOptions()
1007                 commandArgs = []
1008
1009         if commandOptions.test:
1010                 run_doctest()
1011         else:
1012                 run_dialpad()