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