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