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