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