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