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