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