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