Adding some todos and bugs
[gc-dialer] / src / dc_glade.py
index 805e9b7..e9e30c6 100755 (executable)
@@ -18,8 +18,16 @@ You should have received a copy of the GNU Lesser General Public
 License along with this library; if not, write to the Free Software
 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 
 License along with this library; if not, write to the Free Software
 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 
-@bug Completely broken on Maemo
-@todo Add storing of credentials like DoneIt
+@todo Try gogol's bigger tree view items
+@todo Try gogol's wrapping for messages
+@bug Does special notifiers even work?
+@bug Messaging UI is bad(?)
+@bug Not logging back in on network reconnect
+
+@todo Figure out how to integrate with the Maemo contacts app
+@bug Session timeouts are bad, possible solutions:
+       @li For every X minutes, if logged in, attempt login
+       @li Restructure so code handling login/dial/sms is beneath everything else and login attempts are made if those fail
 @todo Add logging support to make debugging issues for people a lot easier
 """
 
 @todo Add logging support to make debugging issues for people a lot easier
 """
 
@@ -34,7 +42,6 @@ import base64
 import ConfigParser
 import itertools
 import warnings
 import ConfigParser
 import itertools
 import warnings
-import traceback
 
 import gtk
 import gtk.glade
 
 import gtk
 import gtk.glade
@@ -44,6 +51,7 @@ try:
 except ImportError:
        hildon = None
 
 except ImportError:
        hildon = None
 
+import constants
 import gtk_toolbox
 
 
 import gtk_toolbox
 
 
@@ -65,15 +73,10 @@ def display_error_message(msg):
 
 class Dialcentral(object):
 
 
 class Dialcentral(object):
 
-       __pretty_app_name__ = "DialCentral"
-       __app_name__ = "dialcentral"
-       __version__ = "0.9.6"
-       __app_magic__ = 0xdeadbeef
-
        _glade_files = [
        _glade_files = [
-               '/usr/lib/dialcentral/dialcentral.glade',
                os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
                os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
                os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
                os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
+               '/usr/lib/dialcentral/dialcentral.glade',
        ]
 
        KEYPAD_TAB = 0
        ]
 
        KEYPAD_TAB = 0
@@ -87,16 +90,12 @@ class Dialcentral(object):
        GV_BACKEND = 2
        BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
 
        GV_BACKEND = 2
        BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
 
-       _data_path = os.path.join(os.path.expanduser("~"), ".dialcentral")
-       _user_settings = "%s/settings.ini" % _data_path
-
        def __init__(self):
                self._initDone = False
                self._connection = None
                self._osso = None
                self._clipboard = gtk.clipboard_get()
 
        def __init__(self):
                self._initDone = False
                self._connection = None
                self._osso = None
                self._clipboard = gtk.clipboard_get()
 
-               self._deviceIsOnline = True
                self._credentials = ("", "")
                self._selectedBackendId = self.NULL_BACKEND
                self._defaultBackendId = self.GC_BACKEND
                self._credentials = ("", "")
                self._selectedBackendId = self.NULL_BACKEND
                self._defaultBackendId = self.GC_BACKEND
@@ -106,6 +105,9 @@ class Dialcentral(object):
                self._messagesViews = None
                self._recentViews = None
                self._contactsViews = None
                self._messagesViews = None
                self._recentViews = None
                self._contactsViews = None
+               self._alarmHandler = None
+               self._ledHandler = None
+               self._originalCurrentLabels = []
 
                for path in self._glade_files:
                        if os.path.isfile(path):
 
                for path in self._glade_files:
                        if os.path.isfile(path):
@@ -125,14 +127,27 @@ class Dialcentral(object):
                self._isFullScreen = False
                if hildon is not None:
                        self._app = hildon.Program()
                self._isFullScreen = False
                if hildon is not None:
                        self._app = hildon.Program()
+                       oldWindow = self._window
                        self._window = hildon.Window()
                        self._window = hildon.Window()
-                       self._widgetTree.get_widget("vbox1").reparent(self._window)
+                       oldWindow.get_child().reparent(self._window)
                        self._app.add_window(self._window)
                        self._app.add_window(self._window)
-                       self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
-                       self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
-                       self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
-                       hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
-                       hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
+
+                       try:
+                               self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
+                               self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
+                               self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
+                       except TypeError, e:
+                               warnings.warn(e.message)
+                       for scrollingWidget in (
+                               'recent_scrolledwindow',
+                               'message_scrolledwindow',
+                               'contacts_scrolledwindow',
+                               "phoneSelectionMessage_scrolledwindow",
+                               "phonetypes_scrolledwindow",
+                               "smsMessage_scrolledwindow",
+                               "smsMessage_scrolledEntry",
+                       ):
+                               hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget(scrollingWidget), True)
 
                        gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
                        menu = gtk.Menu()
 
                        gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
                        menu = gtk.Menu()
@@ -146,20 +161,25 @@ class Dialcentral(object):
                else:
                        pass # warnings.warn("No Hildon", UserWarning, 2)
 
                else:
                        pass # warnings.warn("No Hildon", UserWarning, 2)
 
-               if hildon is not None:
-                       self._window.set_title("Keypad")
-               else:
-                       self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
+               # If under hildon, rely on the application name being shown
+               if hildon is None:
+                       self._window.set_title("%s" % constants.__pretty_app_name__)
 
                callbackMapping = {
                        "on_dialpad_quit": self._on_close,
                }
                self._widgetTree.signal_autoconnect(callbackMapping)
 
 
                callbackMapping = {
                        "on_dialpad_quit": self._on_close,
                }
                self._widgetTree.signal_autoconnect(callbackMapping)
 
-               if self._window:
-                       self._window.connect("destroy", self._on_close)
-                       self._window.show_all()
-                       self._window.set_default_size(800, 300)
+               self._window.connect("destroy", self._on_close)
+               self._window.set_default_size(800, 300)
+               self._window.show_all()
+
+               self._loginSink = gtk_toolbox.threaded_stage(
+                       gtk_toolbox.comap(
+                               self.attempt_login,
+                               gtk_toolbox.null_sink(),
+                       )
+               )
 
                backgroundSetup = threading.Thread(target=self._idle_setup)
                backgroundSetup.setDaemon(True)
 
                backgroundSetup = threading.Thread(target=self._idle_setup)
                backgroundSetup.setDaemon(True)
@@ -169,226 +189,292 @@ class Dialcentral(object):
                """
                If something can be done after the UI loads, push it here so it's not blocking the UI
                """
                """
                If something can be done after the UI loads, push it here so it's not blocking the UI
                """
-               # Barebones UI handlers
-               import null_backend
-               import null_views
-
-               self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
-               with gtk_toolbox.gtk_lock():
-                       self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
-                       self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
-                       self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
-                       self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
-                       self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
-
-                       self._dialpads[self._selectedBackendId].enable()
-                       self._accountViews[self._selectedBackendId].enable()
-                       self._recentViews[self._selectedBackendId].enable()
-                       self._messagesViews[self._selectedBackendId].enable()
-                       self._contactsViews[self._selectedBackendId].enable()
-
-               # Setup maemo specifics
-               try:
-                       import osso
-               except ImportError:
-                       osso = None
-               self._osso = None
-               if osso is not None:
-                       self._osso = osso.Context(Dialcentral.__app_name__, Dialcentral.__version__, False)
-                       device = osso.DeviceState(self._osso)
-                       device.set_device_state_callback(self._on_device_state_change, 0)
-               else:
-                       pass # warnings.warn("No OSSO", UserWarning)
-
-               try:
-                       import conic
-               except ImportError:
-                       conic = None
-               self._connection = None
-               if conic is not None:
-                       self._connection = conic.Connection()
-                       self._connection.connect("connection-event", self._on_connection_change, Dialcentral.__app_magic__)
-                       self._connection.request_connection(conic.CONNECT_FLAG_NONE)
-               else:
-                       pass # warnings.warn("No Internet Connectivity API ", UserWarning)
-
-               # Setup costly backends
-               import gv_backend
-               import gc_backend
-               import file_backend
-               import evo_backend
-               import gc_views
-
                try:
                try:
-                       os.makedirs(self._data_path)
-               except OSError, e:
-                       if e.errno != 17:
-                               raise
-               gcCookiePath = os.path.join(self._data_path, "gc_cookies.txt")
-               gvCookiePath = os.path.join(self._data_path, "gv_cookies.txt")
-               self._defaultBackendId = self._guess_preferred_backend((
-                       (self.GC_BACKEND, gcCookiePath),
-                       (self.GV_BACKEND, gvCookiePath),
-               ))
-
-               self._phoneBackends.update({
-                       self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
-                       self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
-               })
-               with gtk_toolbox.gtk_lock():
-                       unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
-                       unifiedDialpad.set_number("")
-                       self._dialpads.update({
-                               self.GC_BACKEND: unifiedDialpad,
-                               self.GV_BACKEND: unifiedDialpad,
-                       })
-                       self._accountViews.update({
-                               self.GC_BACKEND: gc_views.AccountInfo(
-                                       self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
-                               ),
-                               self.GV_BACKEND: gc_views.AccountInfo(
-                                       self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
-                               ),
-                       })
-                       self._recentViews.update({
-                               self.GC_BACKEND: gc_views.RecentCallsView(
-                                       self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
-                               ),
-                               self.GV_BACKEND: gc_views.RecentCallsView(
-                                       self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
-                               ),
-                       })
-                       self._messagesViews.update({
-                               self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
-                               self.GV_BACKEND: gc_views.MessagesView(
-                                       self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
-                               ),
-                       })
-                       self._contactsViews.update({
-                               self.GC_BACKEND: gc_views.ContactsView(
-                                       self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
-                               ),
-                               self.GV_BACKEND: gc_views.ContactsView(
-                                       self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
-                               ),
+                       # Barebones UI handlers
+                       import null_backend
+                       import null_views
+
+                       self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
+                       with gtk_toolbox.gtk_lock():
+                               self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
+                               self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
+                               self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
+                               self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
+                               self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
+
+                               self._dialpads[self._selectedBackendId].enable()
+                               self._accountViews[self._selectedBackendId].enable()
+                               self._recentViews[self._selectedBackendId].enable()
+                               self._messagesViews[self._selectedBackendId].enable()
+                               self._contactsViews[self._selectedBackendId].enable()
+
+                       # Setup maemo specifics
+                       try:
+                               import osso
+                       except ImportError:
+                               osso = None
+                       self._osso = None
+                       if osso is not None:
+                               self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
+                               device = osso.DeviceState(self._osso)
+                               device.set_device_state_callback(self._on_device_state_change, 0)
+                       else:
+                               pass # warnings.warn("No OSSO", UserWarning, 2)
+
+                       try:
+                               import alarm_handler
+                               self._alarmHandler = alarm_handler.AlarmHandler()
+                       except ImportError:
+                               alarm_handler = None
+                       except Exception:
+                               with gtk_toolbox.gtk_lock():
+                                       self._errorDisplay.push_exception()
+                               alarm_handler = None
+                       if hildon is not None:
+                               import led_handler
+                               self._ledHandler = led_handler.LedHandler()
+
+                       # Setup maemo specifics
+                       try:
+                               import conic
+                       except ImportError:
+                               conic = None
+                       self._connection = None
+                       if conic is not None:
+                               self._connection = conic.Connection()
+                               self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
+                               self._connection.request_connection(conic.CONNECT_FLAG_NONE)
+                       else:
+                               pass # warnings.warn("No Internet Connectivity API ", UserWarning)
+
+                       # Setup costly backends
+                       import gv_backend
+                       import gc_backend
+                       import file_backend
+                       import gc_views
+
+                       try:
+                               os.makedirs(constants._data_path_)
+                       except OSError, e:
+                               if e.errno != 17:
+                                       raise
+                       gcCookiePath = os.path.join(constants._data_path_, "gc_cookies.txt")
+                       gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
+                       self._defaultBackendId = self._guess_preferred_backend((
+                               (self.GC_BACKEND, gcCookiePath),
+                               (self.GV_BACKEND, gvCookiePath),
+                       ))
+
+                       self._phoneBackends.update({
+                               self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
+                               self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
                        })
                        })
+                       with gtk_toolbox.gtk_lock():
+                               unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
+                               unifiedDialpad.set_number("")
+                               self._dialpads.update({
+                                       self.GC_BACKEND: unifiedDialpad,
+                                       self.GV_BACKEND: unifiedDialpad,
+                               })
+                               self._accountViews.update({
+                                       self.GC_BACKEND: gc_views.AccountInfo(
+                                               self._widgetTree, self._phoneBackends[self.GC_BACKEND], None, self._errorDisplay
+                                       ),
+                                       self.GV_BACKEND: gc_views.AccountInfo(
+                                               self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
+                                       ),
+                               })
+                               self._accountViews[self.GC_BACKEND].save_everything = lambda *args: None
+                               self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
+                               self._recentViews.update({
+                                       self.GC_BACKEND: gc_views.RecentCallsView(
+                                               self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
+                                       ),
+                                       self.GV_BACKEND: gc_views.RecentCallsView(
+                                               self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
+                                       ),
+                               })
+                               self._messagesViews.update({
+                                       self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
+                                       self.GV_BACKEND: gc_views.MessagesView(
+                                               self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
+                                       ),
+                               })
+                               self._contactsViews.update({
+                                       self.GC_BACKEND: gc_views.ContactsView(
+                                               self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
+                                       ),
+                                       self.GV_BACKEND: gc_views.ContactsView(
+                                               self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
+                                       ),
+                               })
+
+                       fsContactsPath = os.path.join(constants._data_path_, "contacts")
+                       fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
+                       for backendId in (self.GV_BACKEND, self.GC_BACKEND):
+                               self._dialpads[backendId].number_selected = self._select_action
+                               self._recentViews[backendId].number_selected = self._select_action
+                               self._messagesViews[backendId].number_selected = self._select_action
+                               self._contactsViews[backendId].number_selected = self._select_action
+
+                               addressBooks = [
+                                       self._phoneBackends[backendId],
+                                       fileBackend,
+                               ]
+                               mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
+                               self._contactsViews[backendId].append(mergedBook)
+                               self._contactsViews[backendId].extend(addressBooks)
+                               self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
+
+                       callbackMapping = {
+                               "on_paste": self._on_paste,
+                               "on_refresh": self._on_menu_refresh,
+                               "on_clearcookies_clicked": self._on_clearcookies_clicked,
+                               "on_notebook_switch_page": self._on_notebook_switch_page,
+                               "on_about_activate": self._on_about_activate,
+                       }
+                       self._widgetTree.signal_autoconnect(callbackMapping)
+
+                       with gtk_toolbox.gtk_lock():
+                               self._originalCurrentLabels = [
+                                       self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
+                                       for pageIndex in xrange(self._notebook.get_n_pages())
+                               ]
+                               self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
+                               self._notebookTapHandler.enable()
+                       self._notebookTapHandler.on_tap = self._reset_tab_refresh
+                       self._notebookTapHandler.on_hold = self._on_tab_refresh
+                       self._notebookTapHandler.on_holding = self._set_tab_refresh
+                       self._notebookTapHandler.on_cancel = self._reset_tab_refresh
+
+                       self._initDone = True
+
+                       config = ConfigParser.SafeConfigParser()
+                       config.read(constants._user_settings_)
+                       with gtk_toolbox.gtk_lock():
+                               self.load_settings(config)
+
+                       self._spawn_attempt_login(2)
+               except Exception, e:
+                       with gtk_toolbox.gtk_lock():
+                               self._errorDisplay.push_exception()
+
+       def attempt_login(self, numOfAttempts = 10, force = False):
+               """
+               @todo Handle user notification better like attempting to login and failed login
 
 
-               evoBackend = evo_backend.EvolutionAddressBook()
-               fsContactsPath = os.path.join(self._data_path, "contacts")
-               fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
-               for backendId in (self.GV_BACKEND, self.GC_BACKEND):
-                       self._dialpads[backendId].dial = self._on_dial_clicked
-                       self._recentViews[backendId].number_selected = self._on_number_selected
-                       self._contactsViews[backendId].number_selected = self._on_number_selected
-
-                       addressBooks = [
-                               self._phoneBackends[backendId],
-                               evoBackend,
-                               fileBackend,
-                       ]
-                       mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
-                       self._contactsViews[backendId].append(mergedBook)
-                       self._contactsViews[backendId].extend(addressBooks)
-                       self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
+               @note This must be run outside of the UI lock
+               """
+               try:
+                       assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
+                       assert self._initDone, "Attempting login before app is fully loaded"
 
 
-               callbackMapping = {
-                       "on_paste": self._on_paste,
-                       "on_refresh": self._on_refresh,
-                       "on_clearcookies_clicked": self._on_clearcookies_clicked,
-                       "on_notebook_switch_page": self._on_notebook_switch_page,
-                       "on_about_activate": self._on_about_activate,
-               }
-               self._widgetTree.signal_autoconnect(callbackMapping)
+                       serviceId = self.NULL_BACKEND
+                       loggedIn = False
+                       if not force:
+                               try:
+                                       self.refresh_session()
+                                       serviceId = self._defaultBackendId
+                                       loggedIn = True
+                               except StandardError, e:
+                                       warnings.warn('Session refresh failed with the following message "%s"' % e.message, UserWarning, 2)
+
+                       if not loggedIn:
+                               loggedIn, serviceId = self._login_by_user(numOfAttempts)
+
+                       with gtk_toolbox.gtk_lock():
+                               self._change_loggedin_status(serviceId)
+               except StandardError, e:
+                       with gtk_toolbox.gtk_lock():
+                               self._errorDisplay.push_exception()
 
 
-               self._initDone = True
+       def _spawn_attempt_login(self, *args):
+               self._loginSink.send(args)
 
 
-               config = ConfigParser.SafeConfigParser()
-               config.read(self._user_settings)
-               with gtk_toolbox.gtk_lock():
-                       self.load_settings(config)
+       def refresh_session(self):
+               """
+               @note Thread agnostic
+               """
+               assert self._initDone, "Attempting login before app is fully loaded"
 
 
-               self.attempt_login(2)
+               loggedIn = False
+               if not loggedIn:
+                       loggedIn = self._login_by_cookie()
+               if not loggedIn:
+                       loggedIn = self._login_by_settings()
 
 
-               return False
+               if not loggedIn:
+                       raise RuntimeError("Login Failed")
 
 
-       def attempt_login(self, numOfAttempts = 10):
+       def _login_by_cookie(self):
                """
                """
-               @todo Handle user notification better like attempting to login and failed login
-
-               @note Not meant to be called directly, but run as a seperate thread.
+               @note Thread agnostic
                """
                """
-               assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
-
-               if not self._deviceIsOnline:
-                       warnings.warn("Attempted to login while device was offline")
-                       return False
-               elif self._phoneBackends is None or len(self._phoneBackends) < len(self.BACKENDS):
+               loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
+               if loggedIn:
                        warnings.warn(
                        warnings.warn(
-                               "Attempted to login before initialization is complete, did an event fire early?"
+                               "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
+                               UserWarning, 2
                        )
                        )
-                       return False
+               return loggedIn
 
 
-               loggedIn = False
-               try:
-                       username, password = self._credentials
-                       serviceId = self._defaultBackendId
+       def _login_by_settings(self):
+               """
+               @note Thread agnostic
+               """
+               username, password = self._credentials
+               loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
+               if loggedIn:
+                       self._credentials = username, password
+                       warnings.warn(
+                               "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
+                               UserWarning, 2
+                       )
+               return loggedIn
 
 
-                       # Attempt using the cookies
-                       loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
+       def _login_by_user(self, numOfAttempts):
+               """
+               @note This must be run outside of the UI lock
+               """
+               loggedIn, (username, password) = False, self._credentials
+               tmpServiceId = self.NULL_BACKEND
+               for attemptCount in xrange(numOfAttempts):
                        if loggedIn:
                        if loggedIn:
-                               warnings.warn(
-                                       "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId]
-                               )
-
-                       # Attempt using the settings file
-                       if not loggedIn and username and password:
-                               loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
-                               if loggedIn:
-                                       warnings.warn(
-                                               "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId]
-                                       )
-
-                       # Query the user for credentials
-                       for attemptCount in xrange(numOfAttempts):
-                               if loggedIn:
-                                       break
-                               with gtk_toolbox.gtk_lock():
-                                       availableServices = {
-                                               self.GV_BACKEND: "Google Voice",
-                                               self.GC_BACKEND: "Grand Central",
-                                       }
-                                       credentials = self._credentialsDialog.request_credentials_from(
-                                               availableServices, defaultCredentials = self._credentials
-                                       )
-                                       serviceId, username, password = credentials
-
-                               loggedIn = self._phoneBackends[serviceId].login(username, password)
-                       if 0 < attemptCount:
-                               warnings.warn(
-                                       "Logged into %r through user request" % self._phoneBackends[serviceId]
+                               break
+                       availableServices = (
+                               (self.GV_BACKEND, "Google Voice"),
+                               (self.GC_BACKEND, "Grand Central"),
+                       )
+                       with gtk_toolbox.gtk_lock():
+                               credentials = self._credentialsDialog.request_credentials_from(
+                                       availableServices, defaultCredentials = self._credentials
                                )
                                )
-               except RuntimeError, e:
-                       warnings.warn(traceback.format_exc())
-                       self._errorDisplay.push_exception_with_lock(e)
+                       tmpServiceId, username, password = credentials
+                       loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
 
 
-               with gtk_toolbox.gtk_lock():
-                       if loggedIn:
-                               self._credentials = username, password
-                               self._change_loggedin_status(serviceId)
-                       else:
-                               self._errorDisplay.push_message("Login Failed")
-                               self._change_loggedin_status(self.NULL_BACKEND)
-               return loggedIn
+               if loggedIn:
+                       serviceId = tmpServiceId
+                       self._credentials = username, password
+                       warnings.warn(
+                               "Logged into %r through user request" % self._phoneBackends[serviceId],
+                               UserWarning, 2
+                       )
+               else:
+                       serviceId = self.NULL_BACKEND
 
 
-       def _on_close(self, *args, **kwds):
-               try:
-                       if self._osso is not None:
-                               self._osso.close()
+               return loggedIn, serviceId
 
 
-                       if self._initDone:
-                               self._save_settings()
-               finally:
-                       gtk.main_quit()
+       def _select_action(self, action, number, message):
+               self.refresh_session()
+               if action == "select":
+                       self._dialpads[self._selectedBackendId].set_number(number)
+                       self._notebook.set_current_page(self.KEYPAD_TAB)
+               elif action == "dial":
+                       self._on_dial_clicked(number)
+               elif action == "sms":
+                       self._on_sms_clicked(number, message)
+               else:
+                       assert False, "Unknown action: %s" % action
 
        def _change_loggedin_status(self, newStatus):
                oldStatus = self._selectedBackendId
 
        def _change_loggedin_status(self, newStatus):
                oldStatus = self._selectedBackendId
@@ -417,16 +503,37 @@ class Dialcentral(object):
                """
                @note UI Thread
                """
                """
                @note UI Thread
                """
-               self._defaultBackendId = int(config.get(self.__pretty_app_name__, "active"))
-               blobs = (
-                       config.get(self.__pretty_app_name__, "bin_blob_%i" % i)
-                       for i in xrange(len(self._credentials))
-               )
-               creds = (
-                       base64.b64decode(blob)
-                       for blob in blobs
-               )
-               self._credentials = tuple(creds)
+               try:
+                       self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
+                       blobs = (
+                               config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+                               for i in xrange(len(self._credentials))
+                       )
+                       creds = (
+                               base64.b64decode(blob)
+                               for blob in blobs
+                       )
+                       self._credentials = tuple(creds)
+
+                       if self._alarmHandler is not None:
+                               self._alarmHandler.load_settings(config, "alarm")
+               except ConfigParser.NoOptionError, e:
+                       warnings.warn(
+                               "Settings file %s is missing section %s" % (
+                                       constants._user_settings_,
+                                       e.section,
+                               ),
+                               stacklevel=2
+                       )
+               except ConfigParser.NoSectionError, e:
+                       warnings.warn(
+                               "Settings file %s is missing section %s" % (
+                                       constants._user_settings_,
+                                       e.section,
+                               ),
+                               stacklevel=2
+                       )
+
                for backendId, view in itertools.chain(
                        self._dialpads.iteritems(),
                        self._accountViews.iteritems(),
                for backendId, view in itertools.chain(
                        self._dialpads.iteritems(),
                        self._accountViews.iteritems(),
@@ -435,17 +542,38 @@ class Dialcentral(object):
                        self._contactsViews.iteritems(),
                ):
                        sectionName = "%s - %s" % (backendId, view.name())
                        self._contactsViews.iteritems(),
                ):
                        sectionName = "%s - %s" % (backendId, view.name())
-                       view.load_settings(config, sectionName)
+                       try:
+                               view.load_settings(config, sectionName)
+                       except ConfigParser.NoOptionError, e:
+                               warnings.warn(
+                                       "Settings file %s is missing section %s" % (
+                                               constants._user_settings_,
+                                               e.section,
+                                       ),
+                                       stacklevel=2
+                               )
+                       except ConfigParser.NoSectionError, e:
+                               warnings.warn(
+                                       "Settings file %s is missing section %s" % (
+                                               constants._user_settings_,
+                                               e.section,
+                                       ),
+                                       stacklevel=2
+                               )
 
        def save_settings(self, config):
                """
                @note Thread Agnostic
                """
 
        def save_settings(self, config):
                """
                @note Thread Agnostic
                """
-               config.add_section(self.__pretty_app_name__)
-               config.set(self.__pretty_app_name__, "active", str(self._selectedBackendId))
+               config.add_section(constants.__pretty_app_name__)
+               config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
                for i, value in enumerate(self._credentials):
                        blob = base64.b64encode(value)
                for i, value in enumerate(self._credentials):
                        blob = base64.b64encode(value)
-                       config.set(self.__pretty_app_name__, "bin_blob_%i" % i, blob)
+                       config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
+               config.add_section("alarm")
+               if self._alarmHandler is not None:
+                       self._alarmHandler.save_settings(config, "alarm")
+
                for backendId, view in itertools.chain(
                        self._dialpads.iteritems(),
                        self._accountViews.iteritems(),
                for backendId, view in itertools.chain(
                        self._dialpads.iteritems(),
                        self._accountViews.iteritems(),
@@ -471,9 +599,32 @@ class Dialcentral(object):
                """
                config = ConfigParser.SafeConfigParser()
                self.save_settings(config)
                """
                config = ConfigParser.SafeConfigParser()
                self.save_settings(config)
-               with open(self._user_settings, "wb") as configFile:
+               with open(constants._user_settings_, "wb") as configFile:
                        config.write(configFile)
 
                        config.write(configFile)
 
+       def _refresh_active_tab(self):
+               pageIndex = self._notebook.get_current_page()
+               if pageIndex == self.CONTACTS_TAB:
+                       self._contactsViews[self._selectedBackendId].update(force=True)
+               elif pageIndex == self.RECENT_TAB:
+                       self._recentViews[self._selectedBackendId].update(force=True)
+               elif pageIndex == self.MESSAGES_TAB:
+                       self._messagesViews[self._selectedBackendId].update(force=True)
+
+               if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
+                       if self._ledHandler is not None:
+                               self._ledHandler.off()
+
+       def _on_close(self, *args, **kwds):
+               try:
+                       if self._osso is not None:
+                               self._osso.close()
+
+                       if self._initDone:
+                               self._save_settings()
+               finally:
+                       gtk.main_quit()
+
        def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
                """
                For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
        def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
                """
                For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
@@ -502,14 +653,12 @@ class Dialcentral(object):
                bearer = event.get_bearer_type()
 
                if status == conic.STATUS_CONNECTED:
                bearer = event.get_bearer_type()
 
                if status == conic.STATUS_CONNECTED:
-                       self._deviceIsOnline = True
-                       backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
-                       backgroundLogin.setDaemon(True)
-                       backgroundLogin.start()
+                       if self._initDone:
+                               self._spawn_attempt_login(2)
                elif status == conic.STATUS_DISCONNECTED:
                elif status == conic.STATUS_DISCONNECTED:
-                       self._deviceIsOnline = False
-                       self._defaultBackendId = self._selectedBackendId
-                       self._change_loggedin_status(self.NULL_BACKEND)
+                       if self._initDone:
+                               self._defaultBackendId = self._selectedBackendId
+                               self._change_loggedin_status(self.NULL_BACKEND)
 
        def _on_window_state_change(self, widget, event, *args):
                """
 
        def _on_window_state_change(self, widget, event, *args):
                """
@@ -538,48 +687,52 @@ class Dialcentral(object):
                self._contactsViews[self._selectedBackendId].clear()
                self._change_loggedin_status(self.NULL_BACKEND)
 
                self._contactsViews[self._selectedBackendId].clear()
                self._change_loggedin_status(self.NULL_BACKEND)
 
-               backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
-               backgroundLogin.setDaemon(True)
-               backgroundLogin.start()
+               self._spawn_attempt_login(2, True)
+
+       def _on_notebook_switch_page(self, notebook, page, pageIndex):
+               self._reset_tab_refresh()
+
+               didRecentUpdate = False
+               didMessagesUpdate = False
 
 
-       def _on_notebook_switch_page(self, notebook, page, page_num):
-               if page_num == self.RECENT_TAB:
-                       self._recentViews[self._selectedBackendId].update()
-               elif page_num == self.MESSAGES_TAB:
-                       self._messagesViews[self._selectedBackendId].update()
-               elif page_num == self.CONTACTS_TAB:
+               if pageIndex == self.RECENT_TAB:
+                       didRecentUpdate = self._recentViews[self._selectedBackendId].update()
+               elif pageIndex == self.MESSAGES_TAB:
+                       didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
+               elif pageIndex == self.CONTACTS_TAB:
                        self._contactsViews[self._selectedBackendId].update()
                        self._contactsViews[self._selectedBackendId].update()
-               elif page_num == self.ACCOUNT_TAB:
+               elif pageIndex == self.ACCOUNT_TAB:
                        self._accountViews[self._selectedBackendId].update()
 
                        self._accountViews[self._selectedBackendId].update()
 
-               tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
-               if hildon is not None:
-                       self._window.set_title(tabTitle)
-               else:
-                       self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
+               if didRecentUpdate or didMessagesUpdate:
+                       if self._ledHandler is not None:
+                               self._ledHandler.off()
 
 
-       def _on_number_selected(self, action, number, message):
-               if action == "select":
-                       self._dialpads[self._selectedBackendId].set_number(number)
-                       self._notebook.set_current_page(self.KEYPAD_TAB)
-               elif action == "dial":
-                       self._on_dial_clicked(number)
-               elif action == "sms":
-                       self._on_sms_clicked(number, message)
-               else:
-                       assert False, "Unknown action: %s" % action
+       def _set_tab_refresh(self, *args):
+               pageIndex = self._notebook.get_current_page()
+               child = self._notebook.get_nth_page(pageIndex)
+               self._notebook.get_tab_label(child).set_text("Refresh?")
+               return False
+
+       def _reset_tab_refresh(self, *args):
+               pageIndex = self._notebook.get_current_page()
+               child = self._notebook.get_nth_page(pageIndex)
+               self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
+               return False
+
+       def _on_tab_refresh(self, *args):
+               self._refresh_active_tab()
+               self._reset_tab_refresh()
+               return False
 
        def _on_sms_clicked(self, number, message):
 
        def _on_sms_clicked(self, number, message):
-               """
-               @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
-               """
-               assert number
-               assert message
+               assert number, "No number specified"
+               assert message, "Empty message"
                try:
                        loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
                try:
                        loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
-               except RuntimeError, e:
+               except StandardError, e:
                        loggedIn = False
                        loggedIn = False
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
                        return
 
                if not loggedIn:
                        return
 
                if not loggedIn:
@@ -592,21 +745,21 @@ class Dialcentral(object):
                try:
                        self._phoneBackends[self._selectedBackendId].send_sms(number, message)
                        dialed = True
                try:
                        self._phoneBackends[self._selectedBackendId].send_sms(number, message)
                        dialed = True
-               except RuntimeError, e:
-                       self._errorDisplay.push_exception(e)
+               except StandardError, e:
+                       self._errorDisplay.push_exception()
                except ValueError, e:
                except ValueError, e:
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
+
+               if dialed:
+                       self._dialpads[self._selectedBackendId].clear()
 
        def _on_dial_clicked(self, number):
 
        def _on_dial_clicked(self, number):
-               """
-               @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
-               """
-               assert number
+               assert number, "No number to call"
                try:
                        loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
                try:
                        loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
-               except RuntimeError, e:
+               except StandardError, e:
                        loggedIn = False
                        loggedIn = False
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
                        return
 
                if not loggedIn:
                        return
 
                if not loggedIn:
@@ -617,25 +770,19 @@ class Dialcentral(object):
 
                dialed = False
                try:
 
                dialed = False
                try:
-                       assert self._phoneBackends[self._selectedBackendId].get_callback_number() != ""
+                       assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
                        self._phoneBackends[self._selectedBackendId].dial(number)
                        dialed = True
                        self._phoneBackends[self._selectedBackendId].dial(number)
                        dialed = True
-               except RuntimeError, e:
-                       self._errorDisplay.push_exception(e)
+               except StandardError, e:
+                       self._errorDisplay.push_exception()
                except ValueError, e:
                except ValueError, e:
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
 
                if dialed:
                        self._dialpads[self._selectedBackendId].clear()
 
 
                if dialed:
                        self._dialpads[self._selectedBackendId].clear()
 
-       def _on_refresh(self, *args):
-               page_num = self._notebook.get_current_page()
-               if page_num == self.CONTACTS_TAB:
-                       self._contactsViews[self._selectedBackendId].update(force=True)
-               elif page_num == self.RECENT_TAB:
-                       self._recentViews[self._selectedBackendId].update(force=True)
-               elif page_num == self.MESSAGES_TAB:
-                       self._messagesViews[self._selectedBackendId].update(force=True)
+       def _on_menu_refresh(self, *args):
+               self._refresh_active_tab()
 
        def _on_paste(self, *args):
                contents = self._clipboard.wait_for_text()
 
        def _on_paste(self, *args):
                contents = self._clipboard.wait_for_text()
@@ -643,10 +790,10 @@ class Dialcentral(object):
 
        def _on_about_activate(self, *args):
                dlg = gtk.AboutDialog()
 
        def _on_about_activate(self, *args):
                dlg = gtk.AboutDialog()
-               dlg.set_name(self.__pretty_app_name__)
-               dlg.set_version(self.__version__)
+               dlg.set_name(constants.__pretty_app_name__)
+               dlg.set_version(constants.__version__)
                dlg.set_copyright("Copyright 2008 - LGPL")
                dlg.set_copyright("Copyright 2008 - LGPL")
-               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")
+               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")
                dlg.set_website("http://gc-dialer.garage.maemo.org/")
                dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
                dlg.run()
                dlg.set_website("http://gc-dialer.garage.maemo.org/")
                dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
                dlg.run()
@@ -665,9 +812,12 @@ def run_doctest():
 
 
 def run_dialpad():
 
 
 def run_dialpad():
+       _lock_file = os.path.join(constants._data_path_, ".lock")
+       #with gtk_toolbox.flock(_lock_file, 0):
        gtk.gdk.threads_init()
        gtk.gdk.threads_init()
+
        if hildon is not None:
        if hildon is not None:
-               gtk.set_application_name(Dialcentral.__pretty_app_name__)
+               gtk.set_application_name(constants.__pretty_app_name__)
        handle = Dialcentral()
        gtk.main()
 
        handle = Dialcentral()
        gtk.main()