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