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