4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
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.
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.
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
21 @bug Crashes when switching contact lists, see http://talk.maemo.org/showpost.php?p=312920&postcount=176
22 @bug Refeshing SMS a lot, then go to contacts and send a message, see http://talk.maemo.org/showpost.php?p=312920&postcount=176
23 @bug Can't send sms from dialpad, see http://talk.maemo.org/showpost.php?p=312922&postcount=177
24 @bug Sending an sms from contacts gave an error
25 @bug Getting into a bad state on connection loss, see http://talk.maemo.org/showpost.php?p=312912&postcount=175
27 @todo Figure out how to integrate with the Maemo contacts app
28 @bug Session timeouts are bad, possible solutions:
29 @li For every X minutes, if logged in, attempt login
30 @li Restructure so code handling login/dial/sms is beneath everything else and login attempts are made if those fail
31 @todo Add logging support to make debugging issues for people a lot easier
35 from __future__ import with_statement
58 def getmtime_nothrow(path):
60 return os.path.getmtime(path)
65 def display_error_message(msg):
66 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
68 def close(dialog, response):
70 error_dialog.connect("response", close)
74 class Dialcentral(object):
77 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
78 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
79 '/usr/lib/dialcentral/dialcentral.glade',
91 BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
94 self._initDone = False
95 self._connection = None
97 self._clipboard = gtk.clipboard_get()
99 self._credentials = ("", "")
100 self._selectedBackendId = self.NULL_BACKEND
101 self._defaultBackendId = self.GC_BACKEND
102 self._phoneBackends = None
103 self._dialpads = None
104 self._accountViews = None
105 self._messagesViews = None
106 self._recentViews = None
107 self._contactsViews = None
108 self._alarmHandler = None
109 self._ledHandler = None
110 self._originalCurrentLabels = []
112 for path in self._glade_files:
113 if os.path.isfile(path):
114 self._widgetTree = gtk.glade.XML(path)
117 display_error_message("Cannot find dialcentral.glade")
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)
127 self._isFullScreen = False
128 if hildon is not None:
129 self._app = hildon.Program()
130 oldWindow = self._window
131 self._window = hildon.Window()
132 oldWindow.get_child().reparent(self._window)
133 self._app.add_window(self._window)
136 self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
137 self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
138 self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
140 warnings.warn(e.message)
141 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
142 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('message_scrolledwindow'), True)
143 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
145 gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
147 for child in gtkMenu.get_children():
149 self._window.set_menu(menu)
152 self._window.connect("key-press-event", self._on_key_press)
153 self._window.connect("window-state-event", self._on_window_state_change)
155 pass # warnings.warn("No Hildon", UserWarning, 2)
157 # If under hildon, rely on the application name being shown
159 self._window.set_title("%s" % constants.__pretty_app_name__)
162 "on_dialpad_quit": self._on_close,
164 self._widgetTree.signal_autoconnect(callbackMapping)
166 self._window.connect("destroy", self._on_close)
167 self._window.set_default_size(800, 300)
168 self._window.show_all()
170 self._loginSink = gtk_toolbox.threaded_stage(
173 gtk_toolbox.null_sink(),
177 backgroundSetup = threading.Thread(target=self._idle_setup)
178 backgroundSetup.setDaemon(True)
179 backgroundSetup.start()
181 def _idle_setup(self):
183 If something can be done after the UI loads, push it here so it's not blocking the UI
186 # Barebones UI handlers
190 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
191 with gtk_toolbox.gtk_lock():
192 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
193 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
194 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
195 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
196 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
198 self._dialpads[self._selectedBackendId].enable()
199 self._accountViews[self._selectedBackendId].enable()
200 self._recentViews[self._selectedBackendId].enable()
201 self._messagesViews[self._selectedBackendId].enable()
202 self._contactsViews[self._selectedBackendId].enable()
204 # Setup maemo specifics
211 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
212 device = osso.DeviceState(self._osso)
213 device.set_device_state_callback(self._on_device_state_change, 0)
215 pass # warnings.warn("No OSSO", UserWarning, 2)
219 self._alarmHandler = alarm_handler.AlarmHandler()
223 with gtk_toolbox.gtk_lock():
224 self._errorDisplay.push_exception()
226 if hildon is not None:
228 self._ledHandler = led_handler.LedHandler()
230 # Setup maemo specifics
235 self._connection = None
236 if conic is not None:
237 self._connection = conic.Connection()
238 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
239 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
241 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
243 # Setup costly backends
251 os.makedirs(constants._data_path_)
255 gcCookiePath = os.path.join(constants._data_path_, "gc_cookies.txt")
256 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
257 self._defaultBackendId = self._guess_preferred_backend((
258 (self.GC_BACKEND, gcCookiePath),
259 (self.GV_BACKEND, gvCookiePath),
262 self._phoneBackends.update({
263 self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
264 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
266 with gtk_toolbox.gtk_lock():
267 unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
268 unifiedDialpad.set_number("")
269 self._dialpads.update({
270 self.GC_BACKEND: unifiedDialpad,
271 self.GV_BACKEND: unifiedDialpad,
273 self._accountViews.update({
274 self.GC_BACKEND: gc_views.AccountInfo(
275 self._widgetTree, self._phoneBackends[self.GC_BACKEND], None, self._errorDisplay
277 self.GV_BACKEND: gc_views.AccountInfo(
278 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
281 self._accountViews[self.GC_BACKEND].save_everything = lambda *args: None
282 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
283 self._recentViews.update({
284 self.GC_BACKEND: gc_views.RecentCallsView(
285 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
287 self.GV_BACKEND: gc_views.RecentCallsView(
288 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
291 self._messagesViews.update({
292 self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
293 self.GV_BACKEND: gc_views.MessagesView(
294 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
297 self._contactsViews.update({
298 self.GC_BACKEND: gc_views.ContactsView(
299 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
301 self.GV_BACKEND: gc_views.ContactsView(
302 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
306 evoBackend = evo_backend.EvolutionAddressBook()
307 fsContactsPath = os.path.join(constants._data_path_, "contacts")
308 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
309 for backendId in (self.GV_BACKEND, self.GC_BACKEND):
310 self._dialpads[backendId].number_selected = self._select_action
311 self._recentViews[backendId].number_selected = self._select_action
312 self._messagesViews[backendId].number_selected = self._select_action
313 self._contactsViews[backendId].number_selected = self._select_action
316 self._phoneBackends[backendId],
320 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
321 self._contactsViews[backendId].append(mergedBook)
322 self._contactsViews[backendId].extend(addressBooks)
323 self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
326 "on_paste": self._on_paste,
327 "on_refresh": self._on_menu_refresh,
328 "on_clearcookies_clicked": self._on_clearcookies_clicked,
329 "on_notebook_switch_page": self._on_notebook_switch_page,
330 "on_about_activate": self._on_about_activate,
332 self._widgetTree.signal_autoconnect(callbackMapping)
334 with gtk_toolbox.gtk_lock():
335 self._originalCurrentLabels = [
336 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
337 for pageIndex in xrange(self._notebook.get_n_pages())
339 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
340 self._notebookTapHandler.enable()
341 self._notebookTapHandler.on_tap = self._reset_tab_refresh
342 self._notebookTapHandler.on_hold = self._on_tab_refresh
343 self._notebookTapHandler.on_holding = self._set_tab_refresh
344 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
346 self._initDone = True
348 config = ConfigParser.SafeConfigParser()
349 config.read(constants._user_settings_)
350 with gtk_toolbox.gtk_lock():
351 self.load_settings(config)
353 self._spawn_attempt_login(2)
355 with gtk_toolbox.gtk_lock():
356 self._errorDisplay.push_exception()
358 def attempt_login(self, numOfAttempts = 10, force = False):
360 @todo Handle user notification better like attempting to login and failed login
362 @note This must be run outside of the UI lock
365 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
366 assert self._initDone, "Attempting login before app is fully loaded"
368 serviceId = self.NULL_BACKEND
372 self.refresh_session()
373 serviceId = self._defaultBackendId
375 except StandardError, e:
376 warnings.warn('Session refresh failed with the following message "%s"' % e.message, UserWarning, 2)
379 loggedIn, serviceId = self._login_by_user(numOfAttempts)
381 with gtk_toolbox.gtk_lock():
382 self._change_loggedin_status(serviceId)
383 except StandardError, e:
384 with gtk_toolbox.gtk_lock():
385 self._errorDisplay.push_exception()
387 def _spawn_attempt_login(self, *args):
388 self._loginSink.send(args)
390 def refresh_session(self):
392 @note Thread agnostic
394 assert self._initDone, "Attempting login before app is fully loaded"
398 loggedIn = self._login_by_cookie()
400 loggedIn = self._login_by_settings()
403 raise RuntimeError("Login Failed")
405 def _login_by_cookie(self):
407 @note Thread agnostic
409 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
412 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
417 def _login_by_settings(self):
419 @note Thread agnostic
421 username, password = self._credentials
422 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
424 self._credentials = username, password
426 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
431 def _login_by_user(self, numOfAttempts):
433 @note This must be run outside of the UI lock
435 loggedIn, (username, password) = False, self._credentials
436 tmpServiceId = self.NULL_BACKEND
437 for attemptCount in xrange(numOfAttempts):
440 availableServices = (
441 (self.GV_BACKEND, "Google Voice"),
442 (self.GC_BACKEND, "Grand Central"),
444 with gtk_toolbox.gtk_lock():
445 credentials = self._credentialsDialog.request_credentials_from(
446 availableServices, defaultCredentials = self._credentials
448 tmpServiceId, username, password = credentials
449 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
452 serviceId = tmpServiceId
453 self._credentials = username, password
455 "Logged into %r through user request" % self._phoneBackends[serviceId],
459 serviceId = self.NULL_BACKEND
461 return loggedIn, serviceId
463 def _select_action(self, action, number, message):
464 self.refresh_session()
465 if action == "select":
466 self._dialpads[self._selectedBackendId].set_number(number)
467 self._notebook.set_current_page(self.KEYPAD_TAB)
468 elif action == "dial":
469 self._on_dial_clicked(number)
470 elif action == "sms":
471 self._on_sms_clicked(number, message)
473 assert False, "Unknown action: %s" % action
475 def _change_loggedin_status(self, newStatus):
476 oldStatus = self._selectedBackendId
477 if oldStatus == newStatus:
480 self._dialpads[oldStatus].disable()
481 self._accountViews[oldStatus].disable()
482 self._recentViews[oldStatus].disable()
483 self._messagesViews[oldStatus].disable()
484 self._contactsViews[oldStatus].disable()
486 self._dialpads[newStatus].enable()
487 self._accountViews[newStatus].enable()
488 self._recentViews[newStatus].enable()
489 self._messagesViews[newStatus].enable()
490 self._contactsViews[newStatus].enable()
492 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
493 self._phoneBackends[self._selectedBackendId].set_sane_callback()
494 self._accountViews[self._selectedBackendId].update()
496 self._selectedBackendId = newStatus
498 def load_settings(self, config):
503 self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
505 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
506 for i in xrange(len(self._credentials))
509 base64.b64decode(blob)
512 self._credentials = tuple(creds)
514 if self._alarmHandler is not None:
515 self._alarmHandler.load_settings(config, "alarm")
516 except ConfigParser.NoOptionError, e:
518 "Settings file %s is missing section %s" % (
519 constants._user_settings_,
524 except ConfigParser.NoSectionError, e:
526 "Settings file %s is missing section %s" % (
527 constants._user_settings_,
533 for backendId, view in itertools.chain(
534 self._dialpads.iteritems(),
535 self._accountViews.iteritems(),
536 self._messagesViews.iteritems(),
537 self._recentViews.iteritems(),
538 self._contactsViews.iteritems(),
540 sectionName = "%s - %s" % (backendId, view.name())
542 view.load_settings(config, sectionName)
543 except ConfigParser.NoOptionError, e:
545 "Settings file %s is missing section %s" % (
546 constants._user_settings_,
551 except ConfigParser.NoSectionError, e:
553 "Settings file %s is missing section %s" % (
554 constants._user_settings_,
560 def save_settings(self, config):
562 @note Thread Agnostic
564 config.add_section(constants.__pretty_app_name__)
565 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
566 for i, value in enumerate(self._credentials):
567 blob = base64.b64encode(value)
568 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
569 config.add_section("alarm")
570 if self._alarmHandler is not None:
571 self._alarmHandler.save_settings(config, "alarm")
573 for backendId, view in itertools.chain(
574 self._dialpads.iteritems(),
575 self._accountViews.iteritems(),
576 self._messagesViews.iteritems(),
577 self._recentViews.iteritems(),
578 self._contactsViews.iteritems(),
580 sectionName = "%s - %s" % (backendId, view.name())
581 config.add_section(sectionName)
582 view.save_settings(config, sectionName)
584 def _guess_preferred_backend(self, backendAndCookiePaths):
586 (getmtime_nothrow(path), backendId, path)
587 for backendId, path in backendAndCookiePaths
589 modTimeAndPath.sort()
590 return modTimeAndPath[-1][1]
592 def _save_settings(self):
594 @note Thread Agnostic
596 config = ConfigParser.SafeConfigParser()
597 self.save_settings(config)
598 with open(constants._user_settings_, "wb") as configFile:
599 config.write(configFile)
601 def _refresh_active_tab(self):
602 pageIndex = self._notebook.get_current_page()
603 if pageIndex == self.CONTACTS_TAB:
604 self._contactsViews[self._selectedBackendId].update(force=True)
605 elif pageIndex == self.RECENT_TAB:
606 self._recentViews[self._selectedBackendId].update(force=True)
607 elif pageIndex == self.MESSAGES_TAB:
608 self._messagesViews[self._selectedBackendId].update(force=True)
610 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
611 if self._ledHandler is not None:
612 self._ledHandler.off()
614 def _on_close(self, *args, **kwds):
616 if self._osso is not None:
620 self._save_settings()
624 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
626 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
627 For system_inactivity, we have no background tasks to pause
629 @note Hildon specific
632 for backendId in self.BACKENDS:
633 self._phoneBackends[backendId].clear_caches()
634 self._contactsViews[self._selectedBackendId].clear_caches()
637 if save_unsaved_data or shutdown:
638 self._save_settings()
640 def _on_connection_change(self, connection, event, magicIdentifier):
642 @note Hildon specific
646 status = event.get_status()
647 error = event.get_error()
648 iap_id = event.get_iap_id()
649 bearer = event.get_bearer_type()
651 if status == conic.STATUS_CONNECTED:
653 self._spawn_attempt_login(2)
654 elif status == conic.STATUS_DISCONNECTED:
656 self._defaultBackendId = self._selectedBackendId
657 self._change_loggedin_status(self.NULL_BACKEND)
659 def _on_window_state_change(self, widget, event, *args):
661 @note Hildon specific
663 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
664 self._isFullScreen = True
666 self._isFullScreen = False
668 def _on_key_press(self, widget, event, *args):
670 @note Hildon specific
672 if event.keyval == gtk.keysyms.F6:
673 if self._isFullScreen:
674 self._window.unfullscreen()
676 self._window.fullscreen()
678 def _on_clearcookies_clicked(self, *args):
679 self._phoneBackends[self._selectedBackendId].logout()
680 self._accountViews[self._selectedBackendId].clear()
681 self._recentViews[self._selectedBackendId].clear()
682 self._messagesViews[self._selectedBackendId].clear()
683 self._contactsViews[self._selectedBackendId].clear()
684 self._change_loggedin_status(self.NULL_BACKEND)
686 self._spawn_attempt_login(2, True)
688 def _on_notebook_switch_page(self, notebook, page, pageIndex):
689 self._reset_tab_refresh()
691 didRecentUpdate = False
692 didMessagesUpdate = False
694 if pageIndex == self.RECENT_TAB:
695 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
696 elif pageIndex == self.MESSAGES_TAB:
697 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
698 elif pageIndex == self.CONTACTS_TAB:
699 self._contactsViews[self._selectedBackendId].update()
700 elif pageIndex == self.ACCOUNT_TAB:
701 self._accountViews[self._selectedBackendId].update()
703 if didRecentUpdate or didMessagesUpdate:
704 if self._ledHandler is not None:
705 self._ledHandler.off()
707 def _set_tab_refresh(self, *args):
708 pageIndex = self._notebook.get_current_page()
709 child = self._notebook.get_nth_page(pageIndex)
710 self._notebook.get_tab_label(child).set_text("Refresh?")
713 def _reset_tab_refresh(self, *args):
714 pageIndex = self._notebook.get_current_page()
715 child = self._notebook.get_nth_page(pageIndex)
716 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
719 def _on_tab_refresh(self, *args):
720 self._refresh_active_tab()
721 self._reset_tab_refresh()
724 def _on_sms_clicked(self, number, message):
725 assert number, "No number specified"
726 assert message, "Empty message"
728 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
729 except StandardError, e:
731 self._errorDisplay.push_exception()
735 self._errorDisplay.push_message(
736 "Backend link with grandcentral is not working, please try again"
742 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
744 except StandardError, e:
745 self._errorDisplay.push_exception()
746 except ValueError, e:
747 self._errorDisplay.push_exception()
750 self._dialpads[self._selectedBackendId].clear()
752 def _on_dial_clicked(self, number):
753 assert number, "No number to call"
755 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
756 except StandardError, e:
758 self._errorDisplay.push_exception()
762 self._errorDisplay.push_message(
763 "Backend link with grandcentral is not working, please try again"
769 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
770 self._phoneBackends[self._selectedBackendId].dial(number)
772 except StandardError, e:
773 self._errorDisplay.push_exception()
774 except ValueError, e:
775 self._errorDisplay.push_exception()
778 self._dialpads[self._selectedBackendId].clear()
780 def _on_menu_refresh(self, *args):
781 self._refresh_active_tab()
783 def _on_paste(self, *args):
784 contents = self._clipboard.wait_for_text()
785 self._dialpads[self._selectedBackendId].set_number(contents)
787 def _on_about_activate(self, *args):
788 dlg = gtk.AboutDialog()
789 dlg.set_name(constants.__pretty_app_name__)
790 dlg.set_version(constants.__version__)
791 dlg.set_copyright("Copyright 2008 - LGPL")
792 dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice/Grandcentral account. This application is not affiliated with Google in any way")
793 dlg.set_website("http://gc-dialer.garage.maemo.org/")
794 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
802 failureCount, testCount = doctest.testmod()
804 print "Tests Successful"
811 _lock_file = os.path.join(constants._data_path_, ".lock")
812 #with gtk_toolbox.flock(_lock_file, 0):
813 gtk.gdk.threads_init()
815 if hildon is not None:
816 gtk.set_application_name(constants.__pretty_app_name__)
817 handle = Dialcentral()
821 class DummyOptions(object):
827 if __name__ == "__main__":
828 if len(sys.argv) > 1:
834 if optparse is not None:
835 parser = optparse.OptionParser()
836 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
837 (commandOptions, commandArgs) = parser.parse_args()
839 commandOptions = DummyOptions()
842 if commandOptions.test: