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