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