Adding some todos and bugs
[gc-dialer] / src / dc_glade.py
index b0b6231..e9e30c6 100755 (executable)
@@ -1,26 +1,33 @@
 #!/usr/bin/python2.5
 
 #!/usr/bin/python2.5
 
-# DialCentral - Front end for Google's Grand Central service.
-# Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
-# 
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-# 
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
-# 
-# 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
-
-
 """
 """
-DialCentral: A phone dialer using GrandCentral
-
+DialCentral - Front end for Google's Grand Central service.
+Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+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
+
+@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
 """
 
@@ -31,8 +38,10 @@ import sys
 import gc
 import os
 import threading
 import gc
 import os
 import threading
+import base64
+import ConfigParser
+import itertools
 import warnings
 import warnings
-import traceback
 
 import gtk
 import gtk.glade
 
 import gtk
 import gtk.glade
@@ -42,6 +51,7 @@ try:
 except ImportError:
        hildon = None
 
 except ImportError:
        hildon = None
 
+import constants
 import gtk_toolbox
 
 
 import gtk_toolbox
 
 
@@ -52,66 +62,92 @@ def getmtime_nothrow(path):
                return 0
 
 
                return 0
 
 
-class Dialcentral(object):
+def display_error_message(msg):
+       error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
+
+       def close(dialog, response):
+               dialog.destroy()
+       error_dialog.connect("response", close)
+       error_dialog.run()
 
 
-       __pretty_app_name__ = "DialCentral"
-       __app_name__ = "dialcentral"
-       __version__ = "0.9.5"
-       __app_magic__ = 0xdeadbeef
+
+class Dialcentral(object):
 
        _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
+       RECENT_TAB = 1
+       MESSAGES_TAB = 2
+       CONTACTS_TAB = 3
+       ACCOUNT_TAB = 4
+
        NULL_BACKEND = 0
        GC_BACKEND = 1
        GV_BACKEND = 2
        BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
 
        NULL_BACKEND = 0
        GC_BACKEND = 1
        GV_BACKEND = 2
        BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
 
-       _data_path = os.path.join(os.path.expanduser("~"), ".dialcentral")
-
        def __init__(self):
        def __init__(self):
+               self._initDone = False
                self._connection = None
                self._osso = None
                self._clipboard = gtk.clipboard_get()
 
                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._phoneBackends = None
                self._dialpads = None
                self._accountViews = None
                self._selectedBackendId = self.NULL_BACKEND
                self._defaultBackendId = self.GC_BACKEND
                self._phoneBackends = None
                self._dialpads = None
                self._accountViews = None
+               self._messagesViews = None
                self._recentViews = None
                self._contactsViews = 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):
                                self._widgetTree = gtk.glade.XML(path)
                                break
                else:
 
                for path in self._glade_files:
                        if os.path.isfile(path):
                                self._widgetTree = gtk.glade.XML(path)
                                break
                else:
-                       self.display_error_message("Cannot find dialcentral.glade")
+                       display_error_message("Cannot find dialcentral.glade")
                        gtk.main_quit()
                        return
 
                self._window = self._widgetTree.get_widget("mainWindow")
                self._notebook = self._widgetTree.get_widget("notebook")
                self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
                        gtk.main_quit()
                        return
 
                self._window = self._widgetTree.get_widget("mainWindow")
                self._notebook = self._widgetTree.get_widget("notebook")
                self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
-               self._credentials = gtk_toolbox.LoginWindow(self._widgetTree)
+               self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
 
                self._app = None
                self._isFullScreen = False
                if hildon is not None:
                        self._app = hildon.Program()
 
                self._app = None
                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()
@@ -125,19 +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", gtk.main_quit)
-                       self._window.show_all()
+               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)
@@ -147,189 +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._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._contactsViews[self._selectedBackendId].enable()
-
-               # Setup maemo specifics
                try:
                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)
+                       # 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
 
 
+               @note This must be run outside of the UI lock
+               """
                try:
                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)
+                       assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
+                       assert self._initDone, "Attempting login before app is fully loaded"
 
 
-               # Setup costly backends
-               import gv_backend
-               import gc_backend
-               import file_backend
-               import evo_backend
-               import gc_views
+                       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)
 
 
-               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._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
-                               ),
-                       })
+                       if not loggedIn:
+                               loggedIn, serviceId = self._login_by_user(numOfAttempts)
 
 
-               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])
+                       with gtk_toolbox.gtk_lock():
+                               self._change_loggedin_status(serviceId)
+               except StandardError, e:
+                       with gtk_toolbox.gtk_lock():
+                               self._errorDisplay.push_exception()
 
 
-               callbackMapping = {
-                       "on_paste": self._on_paste,
-                       "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)
+       def _spawn_attempt_login(self, *args):
+               self._loginSink.send(args)
 
 
-               self.attempt_login(2)
+       def refresh_session(self):
+               """
+               @note Thread agnostic
+               """
+               assert self._initDone, "Attempting login before app is fully loaded"
 
 
-               return False
+               loggedIn = False
+               if not loggedIn:
+                       loggedIn = self._login_by_cookie()
+               if not loggedIn:
+                       loggedIn = self._login_by_settings()
 
 
-       def attempt_login(self, numOfAttempts = 10):
-               """
-               @todo Handle user notification better like attempting to login and failed login
+               if not loggedIn:
+                       raise RuntimeError("Login Failed")
 
 
-               @note Not meant to be called directly, but run as a seperate thread.
+       def _login_by_cookie(self):
                """
                """
-               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):
+               @note Thread agnostic
+               """
+               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
-
-               loggedIn = False
-               try:
-                       if self._phoneBackends[self._defaultBackendId].is_authed():
-                               serviceId = self._defaultBackendId
-                               loggedIn = True
-                       for x in xrange(numOfAttempts):
-                               if loggedIn:
-                                       break
-                               with gtk_toolbox.gtk_lock():
-                                       availableServices = {
-                                               self.GV_BACKEND: "Google Voice",
-                                               self.GC_BACKEND: "Grand Central",
-                                       }
-                                       credentials = self._credentials.request_credentials_from(availableServices)
-                                       serviceId, username, password = credentials
-
-                               loggedIn = self._phoneBackends[serviceId].login(username, password)
-               except RuntimeError, e:
-                       warnings.warn(traceback.format_exc())
-                       self._errorDisplay.push_exception_with_lock(e)
-
-               with gtk_toolbox.gtk_lock():
-                       if not loggedIn:
-                               self._errorDisplay.push_message("Login Failed")
-                       self._change_loggedin_status(serviceId if loggedIn else self.NULL_BACKEND)
                return loggedIn
 
                return loggedIn
 
-       def display_error_message(self, msg):
-               error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
-
-               def close(dialog, response, editor):
-                       editor.about_dialog = None
-                       dialog.destroy()
-               error_dialog.connect("response", close, self)
-               error_dialog.run()
+       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
 
 
-       def _on_close(self, *args, **kwds):
-               if self._osso is not None:
-                       self._osso.close()
-               gtk.main_quit()
+       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:
+                               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
+                               )
+                       tmpServiceId, username, password = credentials
+                       loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
+
+               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
+
+               return loggedIn, serviceId
+
+       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
@@ -339,11 +484,13 @@ class Dialcentral(object):
                self._dialpads[oldStatus].disable()
                self._accountViews[oldStatus].disable()
                self._recentViews[oldStatus].disable()
                self._dialpads[oldStatus].disable()
                self._accountViews[oldStatus].disable()
                self._recentViews[oldStatus].disable()
+               self._messagesViews[oldStatus].disable()
                self._contactsViews[oldStatus].disable()
 
                self._dialpads[newStatus].enable()
                self._accountViews[newStatus].enable()
                self._recentViews[newStatus].enable()
                self._contactsViews[oldStatus].disable()
 
                self._dialpads[newStatus].enable()
                self._accountViews[newStatus].enable()
                self._recentViews[newStatus].enable()
+               self._messagesViews[newStatus].enable()
                self._contactsViews[newStatus].enable()
 
                if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
                self._contactsViews[newStatus].enable()
 
                if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
@@ -352,6 +499,92 @@ class Dialcentral(object):
 
                self._selectedBackendId = newStatus
 
 
                self._selectedBackendId = newStatus
 
+       def load_settings(self, config):
+               """
+               @note UI Thread
+               """
+               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(),
+                       self._messagesViews.iteritems(),
+                       self._recentViews.iteritems(),
+                       self._contactsViews.iteritems(),
+               ):
+                       sectionName = "%s - %s" % (backendId, view.name())
+                       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
+               """
+               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)
+                       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(),
+                       self._messagesViews.iteritems(),
+                       self._recentViews.iteritems(),
+                       self._contactsViews.iteritems(),
+               ):
+                       sectionName = "%s - %s" % (backendId, view.name())
+                       config.add_section(sectionName)
+                       view.save_settings(config, sectionName)
+
        def _guess_preferred_backend(self, backendAndCookiePaths):
                modTimeAndPath = [
                        (getmtime_nothrow(path), backendId, path)
        def _guess_preferred_backend(self, backendAndCookiePaths):
                modTimeAndPath = [
                        (getmtime_nothrow(path), backendId, path)
@@ -360,6 +593,38 @@ class Dialcentral(object):
                modTimeAndPath.sort()
                return modTimeAndPath[-1][1]
 
                modTimeAndPath.sort()
                return modTimeAndPath[-1][1]
 
+       def _save_settings(self):
+               """
+               @note Thread Agnostic
+               """
+               config = ConfigParser.SafeConfigParser()
+               self.save_settings(config)
+               with open(constants._user_settings_, "wb") as 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.
@@ -373,6 +638,9 @@ class Dialcentral(object):
                        self._contactsViews[self._selectedBackendId].clear_caches()
                        gc.collect()
 
                        self._contactsViews[self._selectedBackendId].clear_caches()
                        gc.collect()
 
+               if save_unsaved_data or shutdown:
+                       self._save_settings()
+
        def _on_connection_change(self, connection, event, magicIdentifier):
                """
                @note Hildon specific
        def _on_connection_change(self, connection, event, magicIdentifier):
                """
                @note Hildon specific
@@ -385,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):
                """
@@ -417,38 +683,83 @@ class Dialcentral(object):
                self._phoneBackends[self._selectedBackendId].logout()
                self._accountViews[self._selectedBackendId].clear()
                self._recentViews[self._selectedBackendId].clear()
                self._phoneBackends[self._selectedBackendId].logout()
                self._accountViews[self._selectedBackendId].clear()
                self._recentViews[self._selectedBackendId].clear()
+               self._messagesViews[self._selectedBackendId].clear()
                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 == 1:
+               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 == 3:
-                       self._recentViews[self._selectedBackendId].update()
+               elif pageIndex == self.ACCOUNT_TAB:
+                       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, number):
-               self._dialpads[self._selectedBackendId].set_number(number)
-               self._notebook.set_current_page(0)
+       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):
+               assert number, "No number specified"
+               assert message, "Empty message"
+               try:
+                       loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
+               except StandardError, e:
+                       loggedIn = False
+                       self._errorDisplay.push_exception()
+                       return
+
+               if not loggedIn:
+                       self._errorDisplay.push_message(
+                               "Backend link with grandcentral is not working, please try again"
+                       )
+                       return
+
+               dialed = False
+               try:
+                       self._phoneBackends[self._selectedBackendId].send_sms(number, message)
+                       dialed = True
+               except StandardError, e:
+                       self._errorDisplay.push_exception()
+               except ValueError, 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, "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:
@@ -459,17 +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()
-                       self._recentViews[self._selectedBackendId].clear()
+
+       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()
@@ -477,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()
@@ -499,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()