Fixing things so the user can force a logout
[gc-dialer] / src / dc_glade.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 @bug SMS Dialog issues on Hildon
22 @bug Session timeouts are bad, possible solutions:
23         @li For every X minutes, if logged in, attempt login
24         @li Restructure so code handling login/dial/sms is beneath everything else and login attempts are made if those fail
25 @todo Force login on connect if not already done
26 @todo Can't text from dialpad (so can't do any arbitrary number texts)
27 @todo Add logging support to make debugging issues for people a lot easier
28 """
29
30
31 from __future__ import with_statement
32
33 import sys
34 import gc
35 import os
36 import threading
37 import base64
38 import ConfigParser
39 import itertools
40 import warnings
41 import traceback
42
43 import gtk
44 import gtk.glade
45
46 try:
47         import hildon
48 except ImportError:
49         hildon = None
50
51 import gtk_toolbox
52
53
54 def getmtime_nothrow(path):
55         try:
56                 return os.path.getmtime(path)
57         except StandardError:
58                 return 0
59
60
61 def display_error_message(msg):
62         error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
63
64         def close(dialog, response):
65                 dialog.destroy()
66         error_dialog.connect("response", close)
67         error_dialog.run()
68
69
70 class Dialcentral(object):
71
72         __pretty_app_name__ = "DialCentral"
73         __app_name__ = "dialcentral"
74         __version__ = "0.9.6"
75         __app_magic__ = 0xdeadbeef
76
77         _glade_files = [
78                 '/usr/lib/dialcentral/dialcentral.glade',
79                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
80                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
81         ]
82
83         KEYPAD_TAB = 0
84         RECENT_TAB = 1
85         MESSAGES_TAB = 2
86         CONTACTS_TAB = 3
87         ACCOUNT_TAB = 4
88
89         NULL_BACKEND = 0
90         GC_BACKEND = 1
91         GV_BACKEND = 2
92         BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
93
94         _data_path = os.path.join(os.path.expanduser("~"), ".dialcentral")
95         _user_settings = "%s/settings.ini" % _data_path
96
97         def __init__(self):
98                 self._initDone = False
99                 self._connection = None
100                 self._osso = None
101                 self._clipboard = gtk.clipboard_get()
102
103                 self._deviceIsOnline = True
104                 self._credentials = ("", "")
105                 self._selectedBackendId = self.NULL_BACKEND
106                 self._defaultBackendId = self.GC_BACKEND
107                 self._phoneBackends = None
108                 self._dialpads = None
109                 self._accountViews = None
110                 self._messagesViews = None
111                 self._recentViews = None
112                 self._contactsViews = None
113
114                 for path in self._glade_files:
115                         if os.path.isfile(path):
116                                 self._widgetTree = gtk.glade.XML(path)
117                                 break
118                 else:
119                         display_error_message("Cannot find dialcentral.glade")
120                         gtk.main_quit()
121                         return
122
123                 self._window = self._widgetTree.get_widget("mainWindow")
124                 self._notebook = self._widgetTree.get_widget("notebook")
125                 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
126                 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
127
128                 self._app = None
129                 self._isFullScreen = False
130                 if hildon is not None:
131                         self._app = hildon.Program()
132                         oldWindow = self._window
133                         self._window = hildon.Window()
134                         oldWindow.get_child().reparent(self._window)
135                         self._app.add_window(self._window)
136                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
137                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
138                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
139                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
140                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('message_scrolledwindow'), True)
141                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
142
143                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
144                         menu = gtk.Menu()
145                         for child in gtkMenu.get_children():
146                                 child.reparent(menu)
147                         self._window.set_menu(menu)
148                         gtkMenu.destroy()
149
150                         self._window.connect("key-press-event", self._on_key_press)
151                         self._window.connect("window-state-event", self._on_window_state_change)
152                 else:
153                         pass # warnings.warn("No Hildon", UserWarning, 2)
154
155                 if hildon is not None:
156                         self._window.set_title("Keypad")
157                 else:
158                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
159
160                 callbackMapping = {
161                         "on_dialpad_quit": self._on_close,
162                 }
163                 self._widgetTree.signal_autoconnect(callbackMapping)
164
165                 if self._window:
166                         self._window.connect("destroy", self._on_close)
167                         self._window.show_all()
168                         self._window.set_default_size(800, 300)
169
170                 backgroundSetup = threading.Thread(target=self._idle_setup)
171                 backgroundSetup.setDaemon(True)
172                 backgroundSetup.start()
173
174         def _idle_setup(self):
175                 """
176                 If something can be done after the UI loads, push it here so it's not blocking the UI
177                 """
178                 # Barebones UI handlers
179                 import null_backend
180                 import null_views
181
182                 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
183                 with gtk_toolbox.gtk_lock():
184                         self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
185                         self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
186                         self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
187                         self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
188                         self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
189
190                         self._dialpads[self._selectedBackendId].enable()
191                         self._accountViews[self._selectedBackendId].enable()
192                         self._recentViews[self._selectedBackendId].enable()
193                         self._messagesViews[self._selectedBackendId].enable()
194                         self._contactsViews[self._selectedBackendId].enable()
195
196                 # Setup maemo specifics
197                 try:
198                         import osso
199                 except ImportError:
200                         osso = None
201                 self._osso = None
202                 if osso is not None:
203                         self._osso = osso.Context(Dialcentral.__app_name__, Dialcentral.__version__, False)
204                         device = osso.DeviceState(self._osso)
205                         device.set_device_state_callback(self._on_device_state_change, 0)
206                 else:
207                         pass # warnings.warn("No OSSO", UserWarning)
208
209                 # Setup maemo specifics
210                 try:
211                         import conic
212                 except ImportError:
213                         conic = None
214                 self._connection = None
215                 if conic is not None:
216                         self._connection = conic.Connection()
217                         self._connection.connect("connection-event", self._on_connection_change, Dialcentral.__app_magic__)
218                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
219                 else:
220                         pass # warnings.warn("No Internet Connectivity API ", UserWarning)
221
222                 # Setup costly backends
223                 import gv_backend
224                 import gc_backend
225                 import file_backend
226                 import evo_backend
227                 import gc_views
228
229                 try:
230                         os.makedirs(self._data_path)
231                 except OSError, e:
232                         if e.errno != 17:
233                                 raise
234                 gcCookiePath = os.path.join(self._data_path, "gc_cookies.txt")
235                 gvCookiePath = os.path.join(self._data_path, "gv_cookies.txt")
236                 self._defaultBackendId = self._guess_preferred_backend((
237                         (self.GC_BACKEND, gcCookiePath),
238                         (self.GV_BACKEND, gvCookiePath),
239                 ))
240
241                 self._phoneBackends.update({
242                         self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
243                         self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
244                 })
245                 with gtk_toolbox.gtk_lock():
246                         unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
247                         unifiedDialpad.set_number("")
248                         self._dialpads.update({
249                                 self.GC_BACKEND: unifiedDialpad,
250                                 self.GV_BACKEND: unifiedDialpad,
251                         })
252                         self._accountViews.update({
253                                 self.GC_BACKEND: gc_views.AccountInfo(
254                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
255                                 ),
256                                 self.GV_BACKEND: gc_views.AccountInfo(
257                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
258                                 ),
259                         })
260                         self._recentViews.update({
261                                 self.GC_BACKEND: gc_views.RecentCallsView(
262                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
263                                 ),
264                                 self.GV_BACKEND: gc_views.RecentCallsView(
265                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
266                                 ),
267                         })
268                         self._messagesViews.update({
269                                 self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
270                                 self.GV_BACKEND: gc_views.MessagesView(
271                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
272                                 ),
273                         })
274                         self._contactsViews.update({
275                                 self.GC_BACKEND: gc_views.ContactsView(
276                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
277                                 ),
278                                 self.GV_BACKEND: gc_views.ContactsView(
279                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
280                                 ),
281                         })
282
283                 evoBackend = evo_backend.EvolutionAddressBook()
284                 fsContactsPath = os.path.join(self._data_path, "contacts")
285                 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
286                 for backendId in (self.GV_BACKEND, self.GC_BACKEND):
287                         self._dialpads[backendId].dial = self._on_dial_clicked
288                         self._recentViews[backendId].number_selected = self._on_number_selected
289                         self._messagesViews[backendId].number_selected = self._on_number_selected
290                         self._contactsViews[backendId].number_selected = self._on_number_selected
291
292                         addressBooks = [
293                                 self._phoneBackends[backendId],
294                                 evoBackend,
295                                 fileBackend,
296                         ]
297                         mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
298                         self._contactsViews[backendId].append(mergedBook)
299                         self._contactsViews[backendId].extend(addressBooks)
300                         self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
301
302                 callbackMapping = {
303                         "on_paste": self._on_paste,
304                         "on_refresh": self._on_refresh,
305                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
306                         "on_notebook_switch_page": self._on_notebook_switch_page,
307                         "on_about_activate": self._on_about_activate,
308                 }
309                 self._widgetTree.signal_autoconnect(callbackMapping)
310
311                 self._initDone = True
312
313                 config = ConfigParser.SafeConfigParser()
314                 config.read(self._user_settings)
315                 with gtk_toolbox.gtk_lock():
316                         self.load_settings(config)
317
318                 self.attempt_login(2)
319
320                 return False
321
322         def attempt_login(self, numOfAttempts = 10, force = False):
323                 """
324                 @todo Handle user notification better like attempting to login and failed login
325
326                 @note Not meant to be called directly, but run as a seperate thread.
327                 """
328                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
329
330                 if self._phoneBackends is None or len(self._phoneBackends) < len(self.BACKENDS):
331                         warnings.warn(
332                                 "Attempted to login before initialization is complete, did an event fire early?"
333                         )
334                         return False
335
336                 with gtk_toolbox.gtk_lock():
337                         if not self._deviceIsOnline:
338                                 self._errorDisplay.push_message(
339                                         "Unable to login, device is not online"
340                                 )
341                                 return False
342
343                 loggedIn = False
344                 try:
345                         username, password = self._credentials
346                         serviceId = self._defaultBackendId
347
348                         if not force:
349                                 loggedIn, username, password = self._login_by_cookie(username, password)
350                                 if not loggedIn:
351                                         loggedIn, username, password = self._login_by_settings(username, password)
352                         if not loggedIn:
353                                 loggedIn, username, password = self._login_by_user(username, password, numOfAttempts)
354                 except RuntimeError, e:
355                         warnings.warn(traceback.format_exc())
356                         self._errorDisplay.push_exception_with_lock(e)
357
358                 with gtk_toolbox.gtk_lock():
359                         if loggedIn:
360                                 self._credentials = username, password
361                                 self._change_loggedin_status(serviceId)
362                         else:
363                                 self._errorDisplay.push_message("Login Failed")
364                                 self._change_loggedin_status(self.NULL_BACKEND)
365                 return loggedIn
366
367         def _login_by_cookie(self, username, password):
368                 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
369                 if loggedIn:
370                         warnings.warn(
371                                 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
372                                 UserWarning, 2
373                         )
374                 return loggedIn, username, password
375
376         def _login_by_settings(self, username, password):
377                 if username and password:
378                         loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
379                         if loggedIn:
380                                 warnings.warn(
381                                         "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
382                                         UserWarning, 2
383                                 )
384                 return loggedIn, username, password
385
386         def _login_by_user(self, username, password, numOfAttempts):
387                 loggedIn = False
388                 for attemptCount in xrange(numOfAttempts):
389                         if loggedIn:
390                                 break
391                         with gtk_toolbox.gtk_lock():
392                                 availableServices = {
393                                         self.GV_BACKEND: "Google Voice",
394                                         self.GC_BACKEND: "Grand Central",
395                                 }
396                                 credentials = self._credentialsDialog.request_credentials_from(
397                                         availableServices, defaultCredentials = self._credentials
398                                 )
399                                 serviceId, username, password = credentials
400
401                         loggedIn = self._phoneBackends[serviceId].login(username, password)
402                 if 0 < attemptCount:
403                         warnings.warn(
404                                 "Logged into %r through user request" % self._phoneBackends[serviceId],
405                                 UserWarning, 2
406                         )
407                 return loggedIn, username, password
408
409         def _on_close(self, *args, **kwds):
410                 try:
411                         if self._osso is not None:
412                                 self._osso.close()
413
414                         if self._initDone:
415                                 self._save_settings()
416                 finally:
417                         gtk.main_quit()
418
419         def _change_loggedin_status(self, newStatus):
420                 oldStatus = self._selectedBackendId
421                 if oldStatus == newStatus:
422                         return
423
424                 self._dialpads[oldStatus].disable()
425                 self._accountViews[oldStatus].disable()
426                 self._recentViews[oldStatus].disable()
427                 self._messagesViews[oldStatus].disable()
428                 self._contactsViews[oldStatus].disable()
429
430                 self._dialpads[newStatus].enable()
431                 self._accountViews[newStatus].enable()
432                 self._recentViews[newStatus].enable()
433                 self._messagesViews[newStatus].enable()
434                 self._contactsViews[newStatus].enable()
435
436                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
437                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
438                 self._accountViews[self._selectedBackendId].update()
439
440                 self._selectedBackendId = newStatus
441
442         def load_settings(self, config):
443                 """
444                 @note UI Thread
445                 """
446                 self._defaultBackendId = int(config.get(self.__pretty_app_name__, "active"))
447                 try:
448                         blobs = (
449                                 config.get(self.__pretty_app_name__, "bin_blob_%i" % i)
450                                 for i in xrange(len(self._credentials))
451                         )
452                         creds = (
453                                 base64.b64decode(blob)
454                                 for blob in blobs
455                         )
456                         self._credentials = tuple(creds)
457                 except ConfigParser.NoSectionError, e:
458                         warnings.warn(
459                                 "Settings file %s is missing section %s" % (
460                                         self._user_settings,
461                                         e.section,
462                                 ),
463                                 stacklevel=2
464                         )
465
466                 for backendId, view in itertools.chain(
467                         self._dialpads.iteritems(),
468                         self._accountViews.iteritems(),
469                         self._messagesViews.iteritems(),
470                         self._recentViews.iteritems(),
471                         self._contactsViews.iteritems(),
472                 ):
473                         sectionName = "%s - %s" % (backendId, view.name())
474                         try:
475                                 view.load_settings(config, sectionName)
476                         except ConfigParser.NoSectionError, e:
477                                 warnings.warn(
478                                         "Settings file %s is missing section %s" % (
479                                                 self._user_settings,
480                                                 e.section,
481                                         ),
482                                         stacklevel=2
483                                 )
484
485         def save_settings(self, config):
486                 """
487                 @note Thread Agnostic
488                 """
489                 config.add_section(self.__pretty_app_name__)
490                 config.set(self.__pretty_app_name__, "active", str(self._selectedBackendId))
491                 for i, value in enumerate(self._credentials):
492                         blob = base64.b64encode(value)
493                         config.set(self.__pretty_app_name__, "bin_blob_%i" % i, blob)
494                 for backendId, view in itertools.chain(
495                         self._dialpads.iteritems(),
496                         self._accountViews.iteritems(),
497                         self._messagesViews.iteritems(),
498                         self._recentViews.iteritems(),
499                         self._contactsViews.iteritems(),
500                 ):
501                         sectionName = "%s - %s" % (backendId, view.name())
502                         config.add_section(sectionName)
503                         view.save_settings(config, sectionName)
504
505         def _guess_preferred_backend(self, backendAndCookiePaths):
506                 modTimeAndPath = [
507                         (getmtime_nothrow(path), backendId, path)
508                         for backendId, path in backendAndCookiePaths
509                 ]
510                 modTimeAndPath.sort()
511                 return modTimeAndPath[-1][1]
512
513         def _save_settings(self):
514                 """
515                 @note Thread Agnostic
516                 """
517                 config = ConfigParser.SafeConfigParser()
518                 self.save_settings(config)
519                 with open(self._user_settings, "wb") as configFile:
520                         config.write(configFile)
521
522         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
523                 """
524                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
525                 For system_inactivity, we have no background tasks to pause
526
527                 @note Hildon specific
528                 """
529                 if memory_low:
530                         for backendId in self.BACKENDS:
531                                 self._phoneBackends[backendId].clear_caches()
532                         self._contactsViews[self._selectedBackendId].clear_caches()
533                         gc.collect()
534
535                 if save_unsaved_data or shutdown:
536                         self._save_settings()
537
538         def _on_connection_change(self, connection, event, magicIdentifier):
539                 """
540                 @note Hildon specific
541                 """
542                 import conic
543
544                 status = event.get_status()
545                 error = event.get_error()
546                 iap_id = event.get_iap_id()
547                 bearer = event.get_bearer_type()
548
549                 if status == conic.STATUS_CONNECTED:
550                         self._deviceIsOnline = True
551                         if self._initDone:
552                                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
553                                 backgroundLogin.setDaemon(True)
554                                 backgroundLogin.start()
555                 elif status == conic.STATUS_DISCONNECTED:
556                         self._deviceIsOnline = False
557                         if self._initDone:
558                                 self._defaultBackendId = self._selectedBackendId
559                                 self._change_loggedin_status(self.NULL_BACKEND)
560
561         def _on_window_state_change(self, widget, event, *args):
562                 """
563                 @note Hildon specific
564                 """
565                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
566                         self._isFullScreen = True
567                 else:
568                         self._isFullScreen = False
569
570         def _on_key_press(self, widget, event, *args):
571                 """
572                 @note Hildon specific
573                 """
574                 if event.keyval == gtk.keysyms.F6:
575                         if self._isFullScreen:
576                                 self._window.unfullscreen()
577                         else:
578                                 self._window.fullscreen()
579
580         def _on_clearcookies_clicked(self, *args):
581                 self._phoneBackends[self._selectedBackendId].logout()
582                 self._accountViews[self._selectedBackendId].clear()
583                 self._recentViews[self._selectedBackendId].clear()
584                 self._messagesViews[self._selectedBackendId].clear()
585                 self._contactsViews[self._selectedBackendId].clear()
586                 self._change_loggedin_status(self.NULL_BACKEND)
587
588                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2, True])
589                 backgroundLogin.setDaemon(True)
590                 backgroundLogin.start()
591
592         def _on_notebook_switch_page(self, notebook, page, page_num):
593                 if page_num == self.RECENT_TAB:
594                         self._recentViews[self._selectedBackendId].update()
595                 elif page_num == self.MESSAGES_TAB:
596                         self._messagesViews[self._selectedBackendId].update()
597                 elif page_num == self.CONTACTS_TAB:
598                         self._contactsViews[self._selectedBackendId].update()
599                 elif page_num == self.ACCOUNT_TAB:
600                         self._accountViews[self._selectedBackendId].update()
601
602                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
603                 if hildon is not None:
604                         self._window.set_title(tabTitle)
605                 else:
606                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
607
608         def _on_number_selected(self, action, number, message):
609                 if action == "select":
610                         self._dialpads[self._selectedBackendId].set_number(number)
611                         self._notebook.set_current_page(self.KEYPAD_TAB)
612                 elif action == "dial":
613                         self._on_dial_clicked(number)
614                 elif action == "sms":
615                         self._on_sms_clicked(number, message)
616                 else:
617                         assert False, "Unknown action: %s" % action
618
619         def _on_sms_clicked(self, number, message):
620                 """
621                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
622                 """
623                 assert number
624                 assert message
625                 try:
626                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
627                 except RuntimeError, e:
628                         loggedIn = False
629                         self._errorDisplay.push_exception(e)
630                         return
631
632                 if not loggedIn:
633                         self._errorDisplay.push_message(
634                                 "Backend link with grandcentral is not working, please try again"
635                         )
636                         return
637
638                 dialed = False
639                 try:
640                         self._phoneBackends[self._selectedBackendId].send_sms(number, message)
641                         dialed = True
642                 except RuntimeError, e:
643                         self._errorDisplay.push_exception(e)
644                 except ValueError, e:
645                         self._errorDisplay.push_exception(e)
646
647         def _on_dial_clicked(self, number):
648                 """
649                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
650                 """
651                 assert number
652                 try:
653                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
654                 except RuntimeError, e:
655                         loggedIn = False
656                         self._errorDisplay.push_exception(e)
657                         return
658
659                 if not loggedIn:
660                         self._errorDisplay.push_message(
661                                 "Backend link with grandcentral is not working, please try again"
662                         )
663                         return
664
665                 dialed = False
666                 try:
667                         assert self._phoneBackends[self._selectedBackendId].get_callback_number() != ""
668                         self._phoneBackends[self._selectedBackendId].dial(number)
669                         dialed = True
670                 except RuntimeError, e:
671                         self._errorDisplay.push_exception(e)
672                 except ValueError, e:
673                         self._errorDisplay.push_exception(e)
674
675                 if dialed:
676                         self._dialpads[self._selectedBackendId].clear()
677
678         def _on_refresh(self, *args):
679                 page_num = self._notebook.get_current_page()
680                 if page_num == self.CONTACTS_TAB:
681                         self._contactsViews[self._selectedBackendId].update(force=True)
682                 elif page_num == self.RECENT_TAB:
683                         self._recentViews[self._selectedBackendId].update(force=True)
684                 elif page_num == self.MESSAGES_TAB:
685                         self._messagesViews[self._selectedBackendId].update(force=True)
686
687         def _on_paste(self, *args):
688                 contents = self._clipboard.wait_for_text()
689                 self._dialpads[self._selectedBackendId].set_number(contents)
690
691         def _on_about_activate(self, *args):
692                 dlg = gtk.AboutDialog()
693                 dlg.set_name(self.__pretty_app_name__)
694                 dlg.set_version(self.__version__)
695                 dlg.set_copyright("Copyright 2008 - LGPL")
696                 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")
697                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
698                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
699                 dlg.run()
700                 dlg.destroy()
701
702
703 def run_doctest():
704         import doctest
705
706         failureCount, testCount = doctest.testmod()
707         if not failureCount:
708                 print "Tests Successful"
709                 sys.exit(0)
710         else:
711                 sys.exit(1)
712
713
714 def run_dialpad():
715         gtk.gdk.threads_init()
716         if hildon is not None:
717                 gtk.set_application_name(Dialcentral.__pretty_app_name__)
718         handle = Dialcentral()
719         gtk.main()
720
721
722 class DummyOptions(object):
723
724         def __init__(self):
725                 self.test = False
726
727
728 if __name__ == "__main__":
729         if len(sys.argv) > 1:
730                 try:
731                         import optparse
732                 except ImportError:
733                         optparse = None
734
735                 if optparse is not None:
736                         parser = optparse.OptionParser()
737                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
738                         (commandOptions, commandArgs) = parser.parse_args()
739         else:
740                 commandOptions = DummyOptions()
741                 commandArgs = []
742
743         if commandOptions.test:
744                 run_doctest()
745         else:
746                 run_dialpad()