The start of a settings file and removed the timeouts on the different view refreshes
[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                         if self._phoneBackends[self._defaultBackendId].is_authed():
334                                 serviceId = self._defaultBackendId
335                                 loggedIn = True
336                                 username, password = self._credentials
337                         for x in xrange(numOfAttempts):
338                                 if loggedIn:
339                                         break
340                                 with gtk_toolbox.gtk_lock():
341                                         availableServices = {
342                                                 self.GV_BACKEND: "Google Voice",
343                                                 self.GC_BACKEND: "Grand Central",
344                                         }
345                                         credentials = self._credentialsDialog.request_credentials_from(
346                                                 availableServices, defaultCredentials = self._credentials
347                                         )
348                                         serviceId, username, password = credentials
349
350                                 loggedIn = self._phoneBackends[serviceId].login(username, password)
351                 except RuntimeError, e:
352                         warnings.warn(traceback.format_exc())
353                         self._errorDisplay.push_exception_with_lock(e)
354
355                 with gtk_toolbox.gtk_lock():
356                         if loggedIn:
357                                 self._credentials = username, password
358                         else:
359                                 self._errorDisplay.push_message("Login Failed")
360                         self._change_loggedin_status(serviceId if loggedIn else self.NULL_BACKEND)
361                 return loggedIn
362
363         def _on_close(self, *args, **kwds):
364                 try:
365                         if self._osso is not None:
366                                 self._osso.close()
367
368                         if self._initDone:
369                                 self._save_settings()
370                 finally:
371                         gtk.main_quit()
372
373         def _change_loggedin_status(self, newStatus):
374                 oldStatus = self._selectedBackendId
375                 if oldStatus == newStatus:
376                         return
377
378                 self._dialpads[oldStatus].disable()
379                 self._accountViews[oldStatus].disable()
380                 self._recentViews[oldStatus].disable()
381                 self._messagesViews[oldStatus].disable()
382                 self._contactsViews[oldStatus].disable()
383
384                 self._dialpads[newStatus].enable()
385                 self._accountViews[newStatus].enable()
386                 self._recentViews[newStatus].enable()
387                 self._messagesViews[newStatus].enable()
388                 self._contactsViews[newStatus].enable()
389
390                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
391                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
392                 self._accountViews[self._selectedBackendId].update()
393
394                 self._selectedBackendId = newStatus
395
396         def load_settings(self, config):
397                 """
398                 @note UI Thread
399                 """
400                 self._defaultBackendId = int(config.get(self.__pretty_app_name__, "active"))
401                 blobs = (
402                         config.get(self.__pretty_app_name__, "bin_blob_%i" % i)
403                         for i in xrange(len(self._credentials))
404                 )
405                 creds = (
406                         base64.b64decode(blob)
407                         for blob in blobs
408                 )
409                 self._credentials = tuple(creds)
410                 for backendId, view in itertools.chain(
411                         self._dialpads.iteritems(),
412                         self._accountViews.iteritems(),
413                         self._messagesViews.iteritems(),
414                         self._recentViews.iteritems(),
415                         self._contactsViews.iteritems(),
416                 ):
417                         sectionName = "%s - %s" % (backendId, view.name())
418                         view.load_settings(config, sectionName)
419
420         def save_settings(self, config):
421                 """
422                 @note Thread Agnostic
423                 """
424                 config.add_section(self.__pretty_app_name__)
425                 config.set(self.__pretty_app_name__, "active", str(self._selectedBackendId))
426                 for i, value in enumerate(self._credentials):
427                         blob = base64.b64encode(value)
428                         config.set(self.__pretty_app_name__, "bin_blob_%i" % i, blob)
429                 for backendId, view in itertools.chain(
430                         self._dialpads.iteritems(),
431                         self._accountViews.iteritems(),
432                         self._messagesViews.iteritems(),
433                         self._recentViews.iteritems(),
434                         self._contactsViews.iteritems(),
435                 ):
436                         sectionName = "%s - %s" % (backendId, view.name())
437                         config.add_section(sectionName)
438                         view.save_settings(config, sectionName)
439
440         def _guess_preferred_backend(self, backendAndCookiePaths):
441                 modTimeAndPath = [
442                         (getmtime_nothrow(path), backendId, path)
443                         for backendId, path in backendAndCookiePaths
444                 ]
445                 modTimeAndPath.sort()
446                 return modTimeAndPath[-1][1]
447
448         def _save_settings(self):
449                 """
450                 @note Thread Agnostic
451                 """
452                 config = ConfigParser.SafeConfigParser()
453                 self.save_settings(config)
454                 with open(self._user_settings, "wb") as configFile:
455                         config.write(configFile)
456
457         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
458                 """
459                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
460                 For system_inactivity, we have no background tasks to pause
461
462                 @note Hildon specific
463                 """
464                 if memory_low:
465                         for backendId in self.BACKENDS:
466                                 self._phoneBackends[backendId].clear_caches()
467                         self._contactsViews[self._selectedBackendId].clear_caches()
468                         gc.collect()
469
470                 if save_unsaved_data or shutdown:
471                         self._save_settings()
472
473         def _on_connection_change(self, connection, event, magicIdentifier):
474                 """
475                 @note Hildon specific
476                 """
477                 import conic
478
479                 status = event.get_status()
480                 error = event.get_error()
481                 iap_id = event.get_iap_id()
482                 bearer = event.get_bearer_type()
483
484                 if status == conic.STATUS_CONNECTED:
485                         self._deviceIsOnline = True
486                         backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
487                         backgroundLogin.setDaemon(True)
488                         backgroundLogin.start()
489                 elif status == conic.STATUS_DISCONNECTED:
490                         self._deviceIsOnline = False
491                         self._defaultBackendId = self._selectedBackendId
492                         self._change_loggedin_status(self.NULL_BACKEND)
493
494         def _on_window_state_change(self, widget, event, *args):
495                 """
496                 @note Hildon specific
497                 """
498                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
499                         self._isFullScreen = True
500                 else:
501                         self._isFullScreen = False
502
503         def _on_key_press(self, widget, event, *args):
504                 """
505                 @note Hildon specific
506                 """
507                 if event.keyval == gtk.keysyms.F6:
508                         if self._isFullScreen:
509                                 self._window.unfullscreen()
510                         else:
511                                 self._window.fullscreen()
512
513         def _on_clearcookies_clicked(self, *args):
514                 self._phoneBackends[self._selectedBackendId].logout()
515                 self._accountViews[self._selectedBackendId].clear()
516                 self._recentViews[self._selectedBackendId].clear()
517                 self._messagesViews[self._selectedBackendId].clear()
518                 self._contactsViews[self._selectedBackendId].clear()
519                 self._change_loggedin_status(self.NULL_BACKEND)
520
521                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
522                 backgroundLogin.setDaemon(True)
523                 backgroundLogin.start()
524
525         def _on_notebook_switch_page(self, notebook, page, page_num):
526                 if page_num == self.CONTACTS_TAB:
527                         self._contactsViews[self._selectedBackendId].update()
528                 elif page_num == self.RECENT_TAB:
529                         self._recentViews[self._selectedBackendId].update()
530                 elif page_num == self.MESSAGES_TAB:
531                         self._messagesViews[self._selectedBackendId].update()
532
533                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
534                 if hildon is not None:
535                         self._window.set_title(tabTitle)
536                 else:
537                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
538
539         def _on_number_selected(self, action, number, message):
540                 if action == "select":
541                         self._dialpads[self._selectedBackendId].set_number(number)
542                         self._notebook.set_current_page(self.KEYPAD_TAB)
543                 elif action == "dial":
544                         self._on_dial_clicked(number)
545                 elif action == "sms":
546                         self._on_sms_clicked(number, message)
547                 else:
548                         assert False, "Unknown action: %s" % action
549
550         def _on_sms_clicked(self, number, message):
551                 """
552                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
553                 """
554                 assert number
555                 assert message
556                 try:
557                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
558                 except RuntimeError, e:
559                         loggedIn = False
560                         self._errorDisplay.push_exception(e)
561                         return
562
563                 if not loggedIn:
564                         self._errorDisplay.push_message(
565                                 "Backend link with grandcentral is not working, please try again"
566                         )
567                         return
568
569                 dialed = False
570                 try:
571                         self._phoneBackends[self._selectedBackendId].send_sms(number, message)
572                         dialed = True
573                 except RuntimeError, e:
574                         self._errorDisplay.push_exception(e)
575                 except ValueError, e:
576                         self._errorDisplay.push_exception(e)
577
578         def _on_dial_clicked(self, number):
579                 """
580                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
581                 """
582                 assert number
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                         assert self._phoneBackends[self._selectedBackendId].get_callback_number() != ""
599                         self._phoneBackends[self._selectedBackendId].dial(number)
600                         dialed = True
601                 except RuntimeError, e:
602                         self._errorDisplay.push_exception(e)
603                 except ValueError, e:
604                         self._errorDisplay.push_exception(e)
605
606                 if dialed:
607                         self._dialpads[self._selectedBackendId].clear()
608
609         def _on_refresh(self, *args):
610                 page_num = self._notebook.get_current_page()
611                 if page_num == self.CONTACTS_TAB:
612                         self._contactsViews[self._selectedBackendId].update(force=True)
613                 elif page_num == self.RECENT_TAB:
614                         self._recentViews[self._selectedBackendId].update(force=True)
615                 elif page_num == self.MESSAGES_TAB:
616                         self._messagesViews[self._selectedBackendId].update(force=True)
617
618         def _on_paste(self, *args):
619                 contents = self._clipboard.wait_for_text()
620                 self._dialpads[self._selectedBackendId].set_number(contents)
621
622         def _on_about_activate(self, *args):
623                 dlg = gtk.AboutDialog()
624                 dlg.set_name(self.__pretty_app_name__)
625                 dlg.set_version(self.__version__)
626                 dlg.set_copyright("Copyright 2008 - LGPL")
627                 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")
628                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
629                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
630                 dlg.run()
631                 dlg.destroy()
632
633
634 def run_doctest():
635         import doctest
636
637         failureCount, testCount = doctest.testmod()
638         if not failureCount:
639                 print "Tests Successful"
640                 sys.exit(0)
641         else:
642                 sys.exit(1)
643
644
645 def run_dialpad():
646         gtk.gdk.threads_init()
647         if hildon is not None:
648                 gtk.set_application_name(Dialcentral.__pretty_app_name__)
649         handle = Dialcentral()
650         gtk.main()
651
652
653 class DummyOptions(object):
654
655         def __init__(self):
656                 self.test = False
657
658
659 if __name__ == "__main__":
660         if len(sys.argv) > 1:
661                 try:
662                         import optparse
663                 except ImportError:
664                         optparse = None
665
666                 if optparse is not None:
667                         parser = optparse.OptionParser()
668                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
669                         (commandOptions, commandArgs) = parser.parse_args()
670         else:
671                 commandOptions = DummyOptions()
672                 commandArgs = []
673
674         if commandOptions.test:
675                 run_doctest()
676         else:
677                 run_dialpad()