Most of the way there on notification support; can turn off led, can set and delete...
authorepage <eopage@byu.net>
Sat, 15 Aug 2009 03:27:10 +0000 (03:27 +0000)
committerepage <eopage@byu.net>
Sat, 15 Aug 2009 03:27:10 +0000 (03:27 +0000)
git-svn-id: file:///svnroot/gc-dialer/trunk@388 c39d3808-3fe2-4d86-a59f-b7f623ee9f21

src/alarm_handler.py [new file with mode: 0644]
src/alarm_notify.py [new file with mode: 0755]
src/constants.py
src/dc_glade.py
src/dialcentral.glade
src/gc_views.py
src/gtk_toolbox.py
src/led_handler.py [new file with mode: 0755]
src/null_views.py
support/builddeb.py

diff --git a/src/alarm_handler.py b/src/alarm_handler.py
new file mode 100644 (file)
index 0000000..f3aa648
--- /dev/null
@@ -0,0 +1,136 @@
+#!/usr/bin/env python
+
+import os
+import time
+import datetime
+
+import dbus
+import osso.alarmd as alarmd
+
+
+class AlarmHandler(object):
+
+       _INVALID_COOKIE = -1
+       _TITLE = "Dialcentral Notifications"
+       _LAUNCHER = os.path.join(os.path.dirname(__file__), "alarm_notifier.py"),
+       _REPEAT_FOREVER = -1
+       _DEFAULT_FLAGS = (
+               alarmd.ALARM_EVENT_NO_DIALOG |
+               alarmd.ALARM_EVENT_NO_SNOOZE |
+               alarmd.ALARM_EVENT_CONNECTED
+       )
+
+       def __init__(self):
+               self._recurrence = 5
+
+               bus = dbus.SystemBus()
+               self._alarmdDBus = bus.get_object("com.nokia.alarmd", "/com/nokia/alarmd");
+               self._alarmCookie = self._INVALID_COOKIE
+
+       def load_settings(self, config, sectionName):
+               self._recurrence = config.getint(sectionName, "recurrence")
+               self._alarmCookie = config.getint(sectionName, "alarmCookie")
+
+       def save_settings(self, config, sectionName):
+               config.set(sectionName, "recurrence", str(self._recurrence))
+               config.set(sectionName, "alarmCookie", str(self._alarmCookie))
+
+       def apply_settings(self, enabled, recurrence):
+               if recurrence != self._recurrence and enabled != self.isEnabled:
+                       if self.isEnabled:
+                               self.delete_alarm()
+                       elif enabled:
+                               self._set_alarm(recurrence)
+               self._recurrence = int(recurrence)
+
+       def delete_alarm(self):
+               if self._alarmCookie == self._INVALID_COOKIE:
+                       return
+               deleteResult = self._alarmdDBus.del_event(dbus.Int32(self._alarmCookie))
+               self._alarmCookie = self._INVALID_COOKIE
+               assert deleteResult != -1, "Deleting of alarm event failed"
+
+       @property
+       def recurrence(self):
+               return self._recurrence
+
+       @property
+       def isEnabled(self):
+               return self._alarmCookie != self._INVALID_COOKIE
+
+       def _get_start_time(self, recurrence):
+               now = datetime.datetime.now()
+               startTimeMinute = now.minute + recurrence
+               now.replace(minute=startTimeMinute)
+               timestamp = int(time.mktime(now.timetuple()))
+               return timestamp
+
+       def _set_alarm(self, recurrence):
+               alarmTime = self._get_start_time(recurrence)
+
+               #Setup the alarm arguments so that they can be passed to the D-Bus add_event method
+               action = []
+               action.extend(['flags', self._DEFAULT_FLAGS])
+               action.extend(['title', self._TITLE])
+               action.extend(['path', self._LAUNCHER])
+               action.extend([
+                       'arguments',
+                       dbus.Array(
+                               [alarmTime, int(27)],
+                               signature=dbus.Signature('v')
+                       )
+               ])  #int(27) used in place of alarm_index
+
+               event = []
+               event.extend([dbus.ObjectPath('/AlarmdEventRecurring'), dbus.UInt32(4)])
+               event.extend(['action', dbus.ObjectPath('/AlarmdActionExec')])  #use AlarmdActionExec instead of AlarmdActionDbus
+               event.append(dbus.UInt32(len(action) / 2))
+               event.extend(action)
+               event.extend(['time', dbus.Int64(alarmTime)])
+               event.extend(['recurr_interval', dbus.UInt32(recurrence)])
+               event.extend(['recurr_count', dbus.Int32(self._REPEAT_FOREVER)])
+
+               self._alarmCookie = self._alarmdDBus.add_event(*event);
+
+
+def main():
+       import ConfigParser
+       import constants
+       try:
+               import optparse
+       except ImportError:
+               return
+
+       parser = optparse.OptionParser()
+       parser.add_option("-x", "--display", action="store_true", dest="display", help="Display data")
+       parser.add_option("-e", "--enable", action="store_true", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
+       parser.add_option("-d", "--disable", action="store_false", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
+       parser.add_option("-r", "--recurrence", action="store", type="int", dest="recurrence", help="How often the alarm occurs", default=5)
+       (commandOptions, commandArgs) = parser.parse_args()
+
+       alarmHandler = AlarmHandler()
+       config = ConfigParser.SafeConfigParser()
+       config.read(constants._user_settings_)
+       alarmHandler.load_settings(config, "alarm")
+
+       if commandOptions.display:
+               print "Alarm (%s) is %s for every %d minutes" % (
+                       alarmHandler._alarmCookie,
+                       "enabled" if alarmHandler.isEnabled else "disabled",
+                       alarmHandler.recurrence,
+               )
+       else:
+               isEnabled = commandOptions.enabled
+               recurrence = commandOptions.recurrence
+               alarmHandler.apply_settings(isEnabled, recurrence)
+
+               alarmHandler.save_settings(config, "alarm")
+               configFile = open(constants._user_settings_, "wb")
+               try:
+                       config.write(configFile)
+               finally:
+                       configFile.close()
+
+
+if __name__ == "__main__":
+       main()
diff --git a/src/alarm_notify.py b/src/alarm_notify.py
new file mode 100755 (executable)
index 0000000..c2d80a2
--- /dev/null
@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+
+import os
+import filecmp
+import ConfigParser
+import pprint
+
+import constants
+import gv_backend
+
+
+def get_missed(backend):
+       missedPage = backend._browser.download(backend._missedCallsURL)
+       missedJson = pprint.pformat(backend._grab_json(missedPage))
+       return missedJson
+
+
+def get_voicemail(backend):
+       voicemailPage = backend._browser.download(backend._voicemailURL)
+       voicemailJson = pprint.pformat(backend._grab_json(voicemailPage))
+       return voicemailJson
+
+
+def get_sms(backend):
+       smsPage = backend._browser.download(backend._smsURL)
+       smsJson = pprint.pformat(backend._grab_json(smsPage))
+       return smsJson
+
+
+def is_changed(backend, type, get_material):
+       currentMaterial = get_material(backend)
+       previousSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.old.json" % type)
+       currentSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.json" % type)
+
+       try:
+               os.remove(previousSnapshotPath)
+       except OSError, e:
+               # check if failed purely because the old file didn't exist, which is fine
+               if e.errno != 2:
+                       raise
+       try:
+               os.rename(currentSnapshotPath, previousSnapshotPath)
+               previousExists = True
+       except OSError, e:
+               # check if failed purely because the old file didn't exist, which is fine
+               if e.errno != 2:
+                       raise
+               previousExists = False
+
+       currentSnapshot = file(currentSnapshotPath, "w")
+       try:
+               currentSnapshot.write(currentMaterial)
+       finally:
+               currentSnapshot.close()
+
+       if not previousExists:
+               return True
+
+       seemEqual = filecmp.cmp(previousSnapshotPath, currentSnapshotPath)
+       return not seemEqual
+
+
+def notify():
+       gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
+       backend = gv_backend.GVDialer(gvCookiePath)
+
+       loggedIn = False
+
+       if not loggedIn:
+               loggedIn = backend.is_authed()
+
+       config = ConfigParser.SafeConfigParser()
+       config.read(constants._user_settings_)
+       if not loggedIn:
+               import base64
+               try:
+                       blobs = (
+                               config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+                               for i in xrange(2)
+                       )
+                       creds = (
+                               base64.b64decode(blob)
+                               for blob in blobs
+                       )
+                       username, password = tuple(creds)
+                       loggedIn = backend.login(username, password)
+               except ConfigParser.NoOptionError, e:
+                       pass
+               except ConfigParser.NoSectionError, e:
+                       pass
+
+       try:
+               notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
+               notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
+               notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
+       except ConfigParser.NoOptionError, e:
+               notifyOnMissed = False
+               notifyOnVoicemail = False
+               notifyOnSms = False
+       except ConfigParser.NoSectionError, e:
+               notifyOnMissed = False
+               notifyOnVoicemail = False
+               notifyOnSms = False
+
+       assert loggedIn
+       notifySources = []
+       if notifyOnMissed:
+               notifySources.append(("missed", get_missed))
+       if notifyOnVoicemail:
+               notifySources.append(("voicemail", get_voicemail))
+       if notifyOnSms:
+               notifySources.append(("sms", get_sms))
+
+       notifyUser = False
+       for type, get_material in (
+               ("missed", get_missed),
+               ("voicemail", get_voicemail),
+               ("sms", get_sms),
+       ):
+               if is_changed(backend, type, get_material):
+                       notifyUser = True
+
+       if notifyUser:
+               import led_handler
+               led = led_handler.LedHandler()
+               led.on()
+
+
+if __name__ == "__main__":
+       notify()
index 8bdb923..c19dffb 100644 (file)
@@ -1,4 +1,8 @@
+import os
+
 __pretty_app_name__ = "DialCentral"
 __app_name__ = "dialcentral"
 __version__ = "1.0.4"
 __app_magic__ = 0xdeadbeef
+_data_path_ = os.path.join(os.path.expanduser("~"), ".dialcentral")
+_user_settings_ = "%s/settings.ini" % _data_path_
index 245d6c6..14b6db5 100755 (executable)
@@ -39,7 +39,6 @@ import ConfigParser
 import itertools
 import warnings
 
-import gobject
 import gtk
 import gtk.glade
 
@@ -71,9 +70,9 @@ def display_error_message(msg):
 class Dialcentral(object):
 
        _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"),
+               '/usr/lib/dialcentral/dialcentral.glade',
        ]
 
        KEYPAD_TAB = 0
@@ -87,9 +86,6 @@ class Dialcentral(object):
        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
@@ -105,6 +101,8 @@ class Dialcentral(object):
                self._messagesViews = None
                self._recentViews = None
                self._contactsViews = None
+               self._alarmHandler = None
+               self._ledHandler = None
                self._originalCurrentLabels = []
 
                for path in self._glade_files:
@@ -212,6 +210,16 @@ class Dialcentral(object):
                        else:
                                pass # warnings.warn("No OSSO", UserWarning, 2)
 
+                       try:
+                               import alarm_handler
+                               self._alarmHandler = alarm_handler.AlarmHandler()
+                       except ImportError:
+                               alarm_handler = None
+                       if hildon is not None:
+                               import led_handler
+                               self._ledHandler = led_handler.LedHandler()
+                               self._ledHandler.off()
+
                        # Setup maemo specifics
                        try:
                                import conic
@@ -233,12 +241,12 @@ class Dialcentral(object):
                        import gc_views
 
                        try:
-                               os.makedirs(self._data_path)
+                               os.makedirs(constants._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")
+                       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),
@@ -257,12 +265,14 @@ class Dialcentral(object):
                                })
                                self._accountViews.update({
                                        self.GC_BACKEND: gc_views.AccountInfo(
-                                               self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
+                                               self._widgetTree, self._phoneBackends[self.GC_BACKEND], None, self._errorDisplay
                                        ),
                                        self.GV_BACKEND: gc_views.AccountInfo(
-                                               self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
+                                               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
@@ -287,7 +297,7 @@ class Dialcentral(object):
                                })
 
                        evoBackend = evo_backend.EvolutionAddressBook()
-                       fsContactsPath = os.path.join(self._data_path, "contacts")
+                       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
@@ -329,14 +339,14 @@ class Dialcentral(object):
                        self._initDone = True
 
                        config = ConfigParser.SafeConfigParser()
-                       config.read(self._user_settings)
+                       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(e)
+                               self._errorDisplay.push_exception()
 
        def attempt_login(self, numOfAttempts = 10, force = False):
                """
@@ -365,7 +375,7 @@ class Dialcentral(object):
                                self._change_loggedin_status(serviceId)
                except StandardError, e:
                        with gtk_toolbox.gtk_lock():
-                               self._errorDisplay.push_exception(e)
+                               self._errorDisplay.push_exception()
 
        def _spawn_attempt_login(self, *args):
                self._loginSink.send(args)
@@ -493,10 +503,21 @@ class Dialcentral(object):
                                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" % (
-                                       self._user_settings,
+                                       constants._user_settings_,
                                        e.section,
                                ),
                                stacklevel=2
@@ -512,10 +533,18 @@ class Dialcentral(object):
                        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" % (
-                                               self._user_settings,
+                                               constants._user_settings_,
                                                e.section,
                                        ),
                                        stacklevel=2
@@ -530,6 +559,10 @@ class Dialcentral(object):
                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(),
@@ -555,10 +588,13 @@ class Dialcentral(object):
                """
                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)
 
        def _refresh_active_tab(self):
+               if self._ledHandler is not None:
+                       self._ledHandler.off()
+
                pageIndex = self._notebook.get_current_page()
                if pageIndex == self.CONTACTS_TAB:
                        self._contactsViews[self._selectedBackendId].update(force=True)
@@ -676,7 +712,7 @@ class Dialcentral(object):
                        loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
                except StandardError, e:
                        loggedIn = False
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
                        return
 
                if not loggedIn:
@@ -690,9 +726,9 @@ class Dialcentral(object):
                        self._phoneBackends[self._selectedBackendId].send_sms(number, message)
                        dialed = True
                except StandardError, e:
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
                except ValueError, e:
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
 
        def _on_dial_clicked(self, number):
                assert number, "No number to call"
@@ -700,7 +736,7 @@ class Dialcentral(object):
                        loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
                except StandardError, e:
                        loggedIn = False
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
                        return
 
                if not loggedIn:
@@ -715,9 +751,9 @@ class Dialcentral(object):
                        self._phoneBackends[self._selectedBackendId].dial(number)
                        dialed = True
                except StandardError, e:
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
                except ValueError, e:
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
 
                if dialed:
                        self._dialpads[self._selectedBackendId].clear()
@@ -753,11 +789,14 @@ def run_doctest():
 
 
 def run_dialpad():
-       gtk.gdk.threads_init()
-       if hildon is not None:
-               gtk.set_application_name(constants.__pretty_app_name__)
-       handle = Dialcentral()
-       gtk.main()
+       _lock_file = os.path.join(constants._data_path_, ".lock")
+       with gtk_toolbox.flock(_lock_file, 0):
+               gtk.gdk.threads_init()
+
+               if hildon is not None:
+                       gtk.set_application_name(constants.__pretty_app_name__)
+               handle = Dialcentral()
+               gtk.main()
 
 
 class DummyOptions(object):
index 49c4a0a..71e6885 100644 (file)
               <widget class="GtkTable" id="accountview">
                 <property name="visible">True</property>
                 <property name="border_width">11</property>
-                <property name="n_rows">3</property>
+                <property name="n_rows">7</property>
                 <property name="n_columns">2</property>
                 <child>
                   <widget class="GtkLabel" id="gcnumber_display">
                     <property name="visible">True</property>
+                    <property name="xalign">0</property>
                     <property name="label" translatable="yes">No Number Available</property>
                     <property name="use_markup">True</property>
                   </widget>
                   <packing>
                     <property name="left_attach">1</property>
                     <property name="right_attach">2</property>
+                    <property name="x_options">GTK_FILL</property>
                     <property name="y_options"></property>
                   </packing>
                 </child>
                 <child>
                   <widget class="GtkLabel" id="gcnumber_label">
                     <property name="visible">True</property>
-                    <property name="xalign">1</property>
-                    <property name="xpad">5</property>
-                    <property name="label" translatable="yes">Google Voice Number:</property>
-                    <property name="justify">right</property>
+                    <property name="xalign">0</property>
+                    <property name="yalign">0</property>
+                    <property name="ypad">10</property>
+                    <property name="label" translatable="yes">Account Number:</property>
                   </widget>
+                  <packing>
+                    <property name="y_options"></property>
+                  </packing>
                 </child>
                 <child>
                   <widget class="GtkLabel" id="callback_number_label">
                     <property name="visible">True</property>
-                    <property name="xalign">1</property>
-                    <property name="xpad">5</property>
+                    <property name="xalign">0</property>
+                    <property name="yalign">0</property>
+                    <property name="ypad">10</property>
                     <property name="label" translatable="yes">Callback Number:</property>
                   </widget>
                   <packing>
                     <property name="top_attach">1</property>
                     <property name="bottom_attach">2</property>
+                    <property name="y_options"></property>
                   </packing>
                 </child>
                 <child>
-                  <widget class="GtkButton" id="clearcookies">
-                    <property name="label" translatable="yes">Clear Account Information</property>
+                  <widget class="GtkComboBoxEntry" id="callbackcombo">
                     <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="receives_default">True</property>
-                    <signal name="clicked" handler="on_clearcookies_clicked"/>
                   </widget>
                   <packing>
                     <property name="left_attach">1</property>
                     <property name="right_attach">2</property>
+                    <property name="top_attach">1</property>
+                    <property name="bottom_attach">2</property>
+                    <property name="x_options">GTK_FILL</property>
+                    <property name="y_options"></property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkHBox" id="hbox4">
+                    <property name="visible">True</property>
+                    <child>
+                      <widget class="GtkSpinButton" id="minutesEntry">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="invisible_char">&#x25CF;</property>
+                      </widget>
+                      <packing>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <widget class="GtkLabel" id="label2">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">Minutes</property>
+                      </widget>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
+                    <property name="top_attach">3</property>
+                    <property name="bottom_attach">4</property>
+                    <property name="x_options">GTK_FILL</property>
+                    <property name="y_options"></property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkLabel" id="label4">
+                    <property name="visible">True</property>
+                    <property name="xalign">0</property>
+                  </widget>
+                  <packing>
                     <property name="top_attach">2</property>
                     <property name="bottom_attach">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkLabel" id="label5">
+                    <property name="visible">True</property>
+                    <property name="xalign">0</property>
+                  </widget>
+                  <packing>
+                    <property name="top_attach">4</property>
+                    <property name="bottom_attach">5</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkVBox" id="vbox1">
+                    <property name="visible">True</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <widget class="GtkCheckButton" id="missedCheckbox">
+                        <property name="label" translatable="yes">Missed Calls</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="draw_indicator">True</property>
+                      </widget>
+                      <packing>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <widget class="GtkCheckButton" id="voicemailCheckbox">
+                        <property name="label" translatable="yes">Voicemail</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="draw_indicator">True</property>
+                      </widget>
+                      <packing>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <widget class="GtkCheckButton" id="smsCheckbox">
+                        <property name="label" translatable="yes">SMS</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="draw_indicator">True</property>
+                      </widget>
+                      <packing>
+                        <property name="position">2</property>
+                      </packing>
+                    </child>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
+                    <property name="top_attach">4</property>
+                    <property name="bottom_attach">5</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkCheckButton" id="notifyCheckbox">
+                    <property name="label" translatable="yes">Notifications</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">False</property>
+                    <property name="yalign">0</property>
+                    <property name="draw_indicator">True</property>
+                  </widget>
+                  <packing>
+                    <property name="top_attach">3</property>
+                    <property name="bottom_attach">4</property>
+                    <property name="x_options">GTK_FILL</property>
                     <property name="y_options"></property>
                   </packing>
                 </child>
                 <child>
-                  <widget class="GtkComboBoxEntry" id="callbackcombo">
+                  <widget class="GtkButton" id="clearcookies">
+                    <property name="label" translatable="yes">Clear Account Information</property>
                     <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="focus_on_click">False</property>
+                    <signal name="clicked" handler="on_clearcookies_clicked"/>
                   </widget>
                   <packing>
                     <property name="left_attach">1</property>
                     <property name="right_attach">2</property>
-                    <property name="top_attach">1</property>
-                    <property name="bottom_attach">2</property>
+                    <property name="top_attach">6</property>
+                    <property name="bottom_attach">7</property>
                     <property name="x_options">GTK_FILL</property>
                     <property name="y_options"></property>
                   </packing>
                 </child>
                 <child>
+                  <widget class="GtkLabel" id="label3">
+                    <property name="visible">True</property>
+                    <property name="xalign">0</property>
+                  </widget>
+                  <packing>
+                    <property name="top_attach">5</property>
+                    <property name="bottom_attach">6</property>
+                  </packing>
+                </child>
+                <child>
+                  <placeholder/>
+                </child>
+                <child>
+                  <placeholder/>
+                </child>
+                <child>
                   <placeholder/>
                 </child>
               </widget>
index 20a4359..f401775 100644 (file)
@@ -491,7 +491,7 @@ class Dialpad(object):
                        self._prettynumber = make_pretty(self._phonenumber)
                        self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
                except TypeError, e:
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
 
        def clear(self):
                self.set_number("")
@@ -552,31 +552,85 @@ class Dialpad(object):
 
 class AccountInfo(object):
 
-       def __init__(self, widgetTree, backend, errorDisplay):
+       def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
                self._errorDisplay = errorDisplay
                self._backend = backend
                self._isPopulated = False
+               self._alarmHandler = alarmHandler
+               self._notifyOnMissed = False
+               self._notifyOnVoicemail = False
+               self._notifyOnSms = False
 
                self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
                self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
                self._callbackCombo = widgetTree.get_widget("callbackcombo")
                self._onCallbackentryChangedId = 0
 
+               self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
+               self._minutesEntry = widgetTree.get_widget("minutesEntry")
+               self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
+               self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
+               self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
+               self._onNotifyToggled = 0
+               self._onMinutesChanged = 0
+               self._onMissedToggled = 0
+               self._onVoicemailToggled = 0
+               self._onSmsToggled = 0
+
                self._defaultCallback = ""
 
        def enable(self):
                assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
+
                self._accountViewNumberDisplay.set_use_markup(True)
                self.set_account_number("")
+
                self._callbackList.clear()
                self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
+
+               if self._alarmHandler is not None:
+                       self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
+                       self._minutesEntry.set_value(self._alarmHandler.recurrence)
+                       self._missedCheckbox.set_active(self._notifyOnMissed)
+                       self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
+                       self._smsCheckbox.set_active(self._notifyOnSms)
+                       self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
+                       self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed)
+                       self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
+                       self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
+                       self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
+               else:
+                       self._notifyCheckbox.set_sensitive(False)
+                       self._minutesEntry.set_sensitive(False)
+                       self._missedCheckbox.set_sensitive(False)
+                       self._voicemailCheckbox.set_sensitive(False)
+                       self._smsCheckbox.set_sensitive(False)
+
                self.update(force=True)
 
        def disable(self):
                self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
+               self._onCallbackentryChangedId = 0
 
-               self.clear()
+               if self._alarmHandler is not None:
+                       self._notifyCheckbox.disconnect(self._onNotifyToggled)
+                       self._minutesEntry.disconnect(self._onMinutesChanged)
+                       self._missedCheckbox.disconnect(self._onNotifyToggled)
+                       self._voicemailCheckbox.disconnect(self._onNotifyToggled)
+                       self._smsCheckbox.disconnect(self._onNotifyToggled)
+                       self._onNotifyToggled = 0
+                       self._onMinutesChanged = 0
+                       self._onMissedToggled = 0
+                       self._onVoicemailToggled = 0
+                       self._onSmsToggled = 0
+               else:
+                       self._notifyCheckbox.set_sensitive(True)
+                       self._minutesEntry.set_sensitive(True)
+                       self._missedCheckbox.set_sensitive(True)
+                       self._voicemailCheckbox.set_sensitive(True)
+                       self._smsCheckbox.set_sensitive(True)
 
+               self.clear()
                self._callbackList.clear()
 
        def get_selected_callback_number(self):
@@ -599,12 +653,18 @@ class AccountInfo(object):
                self.set_account_number("")
                self._isPopulated = False
 
+       def save_everything(self):
+               raise NotImplementedError
+
        @staticmethod
        def name():
                return "Account Info"
 
        def load_settings(self, config, section):
                self._defaultCallback = config.get(section, "callback")
+               self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
+               self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
+               self._notifyOnSms = config.getboolean(section, "notifyOnSms")
 
        def save_settings(self, config, section):
                """
@@ -612,6 +672,9 @@ class AccountInfo(object):
                """
                callback = self.get_selected_callback_number()
                config.set(section, "callback", callback)
+               config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
+               config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
+               config.set(section, "notifyOnSms", repr(self._notifyOnSms))
 
        def _populate_callback_combo(self):
                self._isPopulated = True
@@ -619,7 +682,7 @@ class AccountInfo(object):
                try:
                        callbackNumbers = self._backend.get_callback_numbers()
                except StandardError, e:
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
                        self._isPopulated = False
                        return
 
@@ -656,13 +719,41 @@ class AccountInfo(object):
                                        UserWarning, 2
                                )
                except StandardError, e:
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
+
+       def _update_alarm_settings(self):
+               try:
+                       isEnabled = self._notifyCheckbox.get_active()
+                       recurrence = self._minutesEntry.get_value()
+                       if isEnabled != self._alarmHandler.isEnabled and recurrence != self.recurrence:
+                               self._alarmHandler.apply_settings(isEnabled, recurrence)
+               finally:
+                       self.save_everything()
+                       self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
+                       self._minutesEntry.set_value(self._alarmHandler.recurrence)
 
        def _on_callbackentry_changed(self, *args):
                text = self.get_selected_callback_number()
                number = make_ugly(text)
                self._set_callback_number(number)
 
+               self.save_everything()
+
+       def _on_notify_toggled(self, *args):
+               self._update_alarm_settings()
+
+       def _on_minutes_changed(self, *args):
+               self._update_alarm_settings()
+
+       def _on_missed_toggled(self, *args):
+               self.save_everything()
+
+       def _on_voicemail_toggled(self, *args):
+               self.save_everything()
+
+       def _on_sms_toggled(self, *args):
+               self.save_everything()
+
 
 class RecentCallsView(object):
 
@@ -772,7 +863,7 @@ class RecentCallsView(object):
                try:
                        recentItems = self._backend.get_recent()
                except StandardError, e:
-                       self._errorDisplay.push_exception_with_lock(e)
+                       self._errorDisplay.push_exception_with_lock()
                        self._isPopulated = False
                        recentItems = []
 
@@ -919,7 +1010,7 @@ class MessagesView(object):
                try:
                        messageItems = self._backend.get_messages()
                except StandardError, e:
-                       self._errorDisplay.push_exception_with_lock(e)
+                       self._errorDisplay.push_exception_with_lock()
                        self._isPopulated = False
                        messageItems = []
 
@@ -1102,7 +1193,7 @@ class ContactsView(object):
                        except StandardError, e:
                                contacts = []
                                self._isPopulated = False
-                               self._errorDisplay.push_exception_with_lock(e)
+                               self._errorDisplay.push_exception_with_lock()
                        for contactId, contactName in contacts:
                                contactType = (addressBook.contact_source_short_name(contactId), )
                                self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
@@ -1132,7 +1223,7 @@ class ContactsView(object):
                        contactDetails = self._addressBook.get_contact_details(contactId)
                except StandardError, e:
                        contactDetails = []
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
                contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
 
                if len(contactPhoneNumbers) == 0:
index 833b097..7080949 100644 (file)
@@ -2,7 +2,10 @@
 
 from __future__ import with_statement
 
+import os
+import errno
 import sys
+import time
 import traceback
 import functools
 import contextlib
@@ -15,6 +18,33 @@ import gtk
 
 
 @contextlib.contextmanager
+def flock(path, timeout=-1):
+       WAIT_FOREVER = -1
+       DELAY = 0.1
+       timeSpent = 0
+
+       acquired = False
+
+       while timeSpent <= timeout or timeout == WAIT_FOREVER:
+               try:
+                       fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
+                       acquired = True
+                       break
+               except OSError, e:
+                       if e.errno != errno.EEXIST:
+                               raise
+               time.sleep(DELAY)
+               timeSpent += DELAY
+
+       assert acquired, "Failed to grab file-lock %s within timeout %d" % (path, timeout)
+
+       try:
+               yield fd
+       finally:
+               os.unlink(path)
+
+
+@contextlib.contextmanager
 def gtk_lock():
        gtk.gdk.threads_enter()
        try:
@@ -344,11 +374,11 @@ class ErrorDisplay(object):
                else:
                        self.__show_message(message)
 
-       def push_exception_with_lock(self, exception = None):
+       def push_exception_with_lock(self, exception = None, stacklevel=3):
                with gtk_lock():
-                       self.push_exception(exception)
+                       self.push_exception(exception, stacklevel=stacklevel)
 
-       def push_exception(self, exception = None):
+       def push_exception(self, exception = None, stacklevel=2):
                if exception is None:
                        userMessage = str(sys.exc_value)
                        warningMessage = str(traceback.format_exc())
@@ -356,7 +386,7 @@ class ErrorDisplay(object):
                        userMessage = str(exception)
                        warningMessage = str(exception)
                self.push_message(userMessage)
-               warnings.warn(warningMessage, stacklevel=2)
+               warnings.warn(warningMessage, stacklevel=stacklevel)
 
        def pop_message(self):
                if 0 < len(self.__messages):
diff --git a/src/led_handler.py b/src/led_handler.py
new file mode 100755 (executable)
index 0000000..211036e
--- /dev/null
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+import dbus
+
+
+class LedHandler(object):
+
+       def __init__(self):
+               self._bus = dbus.SystemBus()
+               self._rawMceRequest = self._bus.get_object("com.nokia.mce", "/com/nokia/mce/request")
+               self._mceRequest = dbus.Interface(self._rawMceRequest, dbus_interface="com.nokia.mce.request")
+
+               self._ledPattern = "PatternCommunicationChat"
+
+       def on(self):
+               self._mceRequest.req_led_pattern_activate(self._ledPattern)
+
+       def off(self):
+               self._mceRequest.req_led_pattern_deactivate(self._ledPattern)
+
+
+if __name__ == "__main__":
+       leds = LedHandler()
+       leds.off()
index 034eeb6..33413cb 100644 (file)
@@ -57,15 +57,33 @@ class AccountInfo(object):
                self._callbackCombo = widgetTree.get_widget("callbackcombo")
                self._clearCookiesButton = widgetTree.get_widget("clearcookies")
 
+               self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
+               self._minutesEntry = widgetTree.get_widget("minutesEntry")
+               self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
+               self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
+               self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
+
        def enable(self):
                self._callbackCombo.set_sensitive(False)
                self._clearCookiesButton.set_sensitive(False)
 
+               self._notifyCheckbox.set_sensitive(False)
+               self._minutesEntry.set_sensitive(False)
+               self._missedCheckbox.set_sensitive(False)
+               self._voicemailCheckbox.set_sensitive(False)
+               self._smsCheckbox.set_sensitive(False)
+
                self._accountViewNumberDisplay.set_text("")
 
        def disable(self):
-               self._clearCookiesButton.set_sensitive(True)
                self._callbackCombo.set_sensitive(True)
+               self._clearCookiesButton.set_sensitive(True)
+
+               self._notifyCheckbox.set_sensitive(True)
+               self._minutesEntry.set_sensitive(True)
+               self._missedCheckbox.set_sensitive(True)
+               self._voicemailCheckbox.set_sensitive(True)
+               self._smsCheckbox.set_sensitive(True)
 
        @staticmethod
        def update():
index 9fadb93..9a32ec0 100755 (executable)
@@ -22,6 +22,7 @@ __changelog__ = '''
 * "Back" button and tabs now visually indicate when they've entered a "hold" state
 * Fixed the duplicate title on Maemo
 * Removing some device connection observer code due to high bug to low benefit ratio
+* Notification support
 
 1.0.3
 * Holding down a tab for a second will now force a refresh