Merge branch 'settings'
authorEd Page <eopage@byu.net>
Tue, 19 Apr 2011 23:50:44 +0000 (18:50 -0500)
committerEd Page <eopage@byu.net>
Tue, 19 Apr 2011 23:50:44 +0000 (18:50 -0500)
35 files changed:
data/LICENSE
data/bell.flac [new file with mode: 0644]
data/bell.wav [new file with mode: 0644]
hand_tests/qtcontacts.py [new file with mode: 0644]
hand_tests/threading.py [new file with mode: 0755]
src/alarm_handler.py
src/alarm_notify.py
src/backends/file_backend.py
src/backends/gv_backend.py
src/backends/gvoice/browser_emu.py
src/backends/gvoice/gvoice.py
src/backends/null_backend.py
src/backends/qt_backend.py [new file with mode: 0644]
src/call_handler.py [new file with mode: 0644]
src/constants.py
src/dialcentral.py
src/dialcentral_qt.py
src/dialogs.py
src/examples/log_notifier.py
src/gv_views.py
src/led_handler.py
src/session.py
src/stream_gst.py [new file with mode: 0644]
src/stream_handler.py [new file with mode: 0644]
src/stream_null.py [new file with mode: 0644]
src/stream_osso.py [new file with mode: 0644]
src/util/concurrent.py
src/util/go_utils.py
src/util/qore_utils.py
src/util/qt_compat.py [new file with mode: 0644]
src/util/qtpie.py
src/util/qtpieboard.py
src/util/qui_utils.py
src/util/qwrappers.py
support/builddeb.py

index 7cbf876..08e7d3f 100644 (file)
@@ -1,3 +1,7 @@
 http://www.gentleface.com/free_icon_set.html
 The Creative Commons Attribution-NonCommercial -- FREE
 http://creativecommons.org/licenses/by-nc-nd/3.0/
+
+Sound:
+http://www.freesound.org/samplesViewSingle.php?id=2166
+http://creativecommons.org/licenses/sampling+/1.0/
diff --git a/data/bell.flac b/data/bell.flac
new file mode 100644 (file)
index 0000000..419420e
Binary files /dev/null and b/data/bell.flac differ
diff --git a/data/bell.wav b/data/bell.wav
new file mode 100644 (file)
index 0000000..6b7fc1b
Binary files /dev/null and b/data/bell.wav differ
diff --git a/hand_tests/qtcontacts.py b/hand_tests/qtcontacts.py
new file mode 100644 (file)
index 0000000..ace8436
--- /dev/null
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import QtMobility.Contacts as QtContacts
+
+
+class QtContactsAddressBook(object):
+
+       def __init__(self, name, uri):
+               self._name = name
+               self._uri = uri
+               self._manager = QtContacts.QContactManager.fromUri(uri)
+               self._contacts = None
+
+       @property
+       def name(self):
+               return self._name
+
+       @property
+       def error(self):
+               return self._manager.error()
+
+       def update_account(self, force = True):
+               if not force and self._contacts is not None:
+                       return
+               self._contacts = dict(self._get_contacts())
+
+       def get_contacts(self):
+               if self._contacts is None:
+                       self._contacts = dict(self._get_contacts())
+               return self._contacts
+
+       def _get_contacts(self):
+               contacts = self._manager.contacts()
+               for contact in contacts:
+                       contactId = contact.localId()
+                       contactName = contact.displayLabel()
+                       phoneDetails = contact.details(QtContacts.QContactPhoneNumber().DefinitionName)
+                       phones = [{"phoneType": "Phone", "phoneNumber": phone.value(QtContacts.QContactPhoneNumber().FieldNumber)} for phone in phoneDetails]
+                       contactDetails = phones
+                       if 0 < len(contactDetails):
+                               yield str(contactId), {
+                                       "contactId": str(contactId),
+                                       "name": contactName,
+                                       "numbers": contactDetails,
+                               }
+
+
+class _QtContactsAddressBookFactory(object):
+
+       def __init__(self):
+               self._availableManagers = {}
+
+               availableMgrs = QtContacts.QContactManager.availableManagers()
+               availableMgrs.remove("invalid")
+               for managerName in availableMgrs:
+                       params = {}
+                       managerUri = QtContacts.QContactManager.buildUri(managerName, params)
+                       self._availableManagers[managerName] =  managerUri
+
+       def get_addressbooks(self):
+               for name, uri in self._availableManagers.iteritems():
+                       book = QtContactsAddressBook(name, uri)
+                       if book.error:
+                               print "Could not load %r due to %r" % (name, book.error)
+                       else:
+                               yield book
+
+
+class _EmptyAddressBookFactory(object):
+
+       def get_addressbooks(self):
+               if False:
+                       yield None
+
+
+if QtContacts is not None:
+       QtContactsAddressBookFactory = _QtContactsAddressBookFactory
+else:
+       QtContactsAddressBookFactory = _EmptyAddressBookFactory
+       print "QtContacts support not available"
+
+
+if __name__ == "__main__":
+       factory = QtContactsAddressBookFactory()
+       books = factory.get_addressbooks()
+       for book in books:
+               print book.name
+               print book.get_contacts()
diff --git a/hand_tests/threading.py b/hand_tests/threading.py
new file mode 100755 (executable)
index 0000000..22084ed
--- /dev/null
@@ -0,0 +1,157 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import functools
+import time
+
+
+FORCE_PYQT = False
+DECORATE = True
+
+
+try:
+       if FORCE_PYQT:
+               raise ImportError()
+       import PySide.QtCore as _QtCore
+       QtCore = _QtCore
+       USES_PYSIDE = True
+except ImportError:
+       import sip
+       sip.setapi('QString', 2)
+       sip.setapi('QVariant', 2)
+       import PyQt4.QtCore as _QtCore
+       QtCore = _QtCore
+       USES_PYSIDE = False
+
+
+if USES_PYSIDE:
+       Signal = QtCore.Signal
+       Slot = QtCore.Slot
+       Property = QtCore.Property
+else:
+       Signal = QtCore.pyqtSignal
+       Slot = QtCore.pyqtSlot
+       Property = QtCore.pyqtProperty
+
+
+def log_exception():
+
+       def log_exception_decorator(func):
+
+               @functools.wraps(func)
+               def wrapper(*args, **kwds):
+                       try:
+                               return func(*args, **kwds)
+                       except Exception:
+                               print "Exception", func.__name__
+                               raise
+
+               if DECORATE:
+                       return wrapper
+               else:
+                       return func
+
+       return log_exception_decorator
+
+
+class QThread44(QtCore.QThread):
+       """
+       This is to imitate QThread in Qt 4.4+ for when running on older version
+       See http://labs.trolltech.com/blogs/2010/06/17/youre-doing-it-wrong
+       (On Lucid I have Qt 4.7 and this is still an issue)
+       """
+
+       def __init__(self, parent = None):
+               QtCore.QThread.__init__(self, parent)
+
+       def run(self):
+               self.exec_()
+
+
+class Producer(QtCore.QObject):
+
+       data = Signal(int)
+       done = Signal()
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+
+       @Slot()
+       @log_exception()
+       def process(self):
+               print "Starting producer"
+               for i in xrange(10):
+                       self.data.emit(i)
+                       time.sleep(0.1)
+               self.done.emit()
+
+
+class Consumer(QtCore.QObject):
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+
+       @Slot()
+       @log_exception()
+       def process(self):
+               print "Starting consumer"
+
+       @Slot()
+       @log_exception()
+       def print_done(self):
+               print "Done"
+
+       @Slot(int)
+       @log_exception()
+       def print_data(self, i):
+               print i
+
+
+def run_producer_consumer():
+       app = QtCore.QCoreApplication([])
+
+       producerThread = QThread44()
+       producer = Producer()
+       producer.moveToThread(producerThread)
+       producerThread.started.connect(producer.process)
+
+       consumerThread = QThread44()
+       consumer = Consumer()
+       consumer.moveToThread(consumerThread)
+       consumerThread.started.connect(consumer.process)
+
+       producer.data.connect(consumer.print_data)
+       producer.done.connect(consumer.print_done)
+
+       @Slot()
+       @log_exception()
+       def producer_done():
+               print "Shutting down"
+               producerThread.quit()
+               consumerThread.quit()
+               print "Done"
+       producer.done.connect(producer_done)
+
+       count = [0]
+
+       @Slot()
+       @log_exception()
+       def thread_done():
+               print "Thread done"
+               count[0] += 1
+               if count[0] == 2:
+                       print "Quitting"
+                       app.exit(0)
+               print "Done"
+       producerThread.finished.connect(thread_done)
+       consumerThread.finished.connect(thread_done)
+
+       producerThread.start()
+       consumerThread.start()
+       print "Status %s" % app.exec_()
+
+
+if __name__ == "__main__":
+       run_producer_consumer()
index 70854ff..a79f992 100644 (file)
@@ -6,6 +6,8 @@ import datetime
 import ConfigParser
 import logging
 
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
 import dbus
 
 
@@ -113,10 +115,15 @@ class _FremantleAlarmHandler(object):
                        pass
 
        def save_settings(self, config, sectionName):
-               config.set(sectionName, "recurrence", str(self._recurrence))
-               config.set(sectionName, "alarmCookie", str(self._alarmCookie))
-               launcher = self._launcher if self._launcher != self._LAUNCHER else ""
-               config.set(sectionName, "notifier", launcher)
+               try:
+                       config.set(sectionName, "recurrence", str(self._recurrence))
+                       config.set(sectionName, "alarmCookie", str(self._alarmCookie))
+                       launcher = self._launcher if self._launcher != self._LAUNCHER else ""
+                       config.set(sectionName, "notifier", launcher)
+               except ConfigParser.NoOptionError:
+                       pass
+               except ConfigParser.NoSectionError:
+                       pass
 
        def apply_settings(self, enabled, recurrence):
                if recurrence != self._recurrence or enabled != self.isEnabled:
@@ -255,39 +262,68 @@ class _DiabloAlarmHandler(object):
                assert deleteResult != -1, "Deleting of alarm event failed"
 
 
-class _NoneAlarmHandler(object):
+class _ApplicationAlarmHandler(object):
 
-       _INVALID_COOKIE = -1
        _REPEAT_FOREVER = -1
-       _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
+       _MIN_TO_MS_FACTORY = 1000 * 60
+
+       def __init__(self):
+               self._timer = QtCore.QTimer()
+               self._timer.setSingleShot(False)
+               self._timer.setInterval(5 * self._MIN_TO_MS_FACTORY)
+
+       def load_settings(self, config, sectionName):
+               try:
+                       self._timer.setInterval(config.getint(sectionName, "recurrence") * self._MIN_TO_MS_FACTORY)
+               except ConfigParser.NoOptionError:
+                       pass
+               except ConfigParser.NoSectionError:
+                       pass
+               self._timer.start()
+
+       def save_settings(self, config, sectionName):
+               config.set(sectionName, "recurrence", str(self.recurrence))
+
+       def apply_settings(self, enabled, recurrence):
+               self._timer.setInterval(recurrence * self._MIN_TO_MS_FACTORY)
+               if enabled:
+                       self._timer.start()
+               else:
+                       self._timer.stop()
+
+       @property
+       def notifySignal(self):
+               return self._timer.timeout
+
+       @property
+       def recurrence(self):
+               return int(self._timer.interval() / self._MIN_TO_MS_FACTORY)
+
+       @property
+       def isEnabled(self):
+               return self._timer.isActive()
+
+
+class _NoneAlarmHandler(object):
 
        def __init__(self):
-               self._alarmCookie = 0
+               self._enabled = False
                self._recurrence = 5
-               self._alarmCookie = self._INVALID_COOKIE
-               self._launcher = self._LAUNCHER
 
        def load_settings(self, config, sectionName):
                try:
                        self._recurrence = config.getint(sectionName, "recurrence")
-                       self._alarmCookie = config.getint(sectionName, "alarmCookie")
-                       launcher = config.get(sectionName, "notifier")
-                       if launcher:
-                               self._launcher = launcher
+                       self._enabled = True
                except ConfigParser.NoOptionError:
                        pass
                except ConfigParser.NoSectionError:
                        pass
 
        def save_settings(self, config, sectionName):
-               config.set(sectionName, "recurrence", str(self._recurrence))
-               config.set(sectionName, "alarmCookie", str(self._alarmCookie))
-               launcher = self._launcher if self._launcher != self._LAUNCHER else ""
-               config.set(sectionName, "notifier", launcher)
+               config.set(sectionName, "recurrence", str(self.recurrence))
 
        def apply_settings(self, enabled, recurrence):
-               self._alarmCookie = 0 if enabled else self._INVALID_COOKIE
-               self._recurrence = recurrence
+               self._enabled = enabled
 
        @property
        def recurrence(self):
@@ -295,16 +331,91 @@ class _NoneAlarmHandler(object):
 
        @property
        def isEnabled(self):
-               return self._alarmCookie != self._INVALID_COOKIE
+               return self._enabled
 
 
-AlarmHandler = {
+_BACKGROUND_ALARM_FACTORY = {
        _FREMANTLE_ALARM: _FremantleAlarmHandler,
        _DIABLO_ALARM: _DiabloAlarmHandler,
-       _NO_ALARM: _NoneAlarmHandler,
+       _NO_ALARM: None,
 }[ALARM_TYPE]
 
 
+class AlarmHandler(object):
+
+       ALARM_NONE = "No Alert"
+       ALARM_BACKGROUND = "Background Alert"
+       ALARM_APPLICATION = "Application Alert"
+       ALARM_TYPES = [ALARM_NONE, ALARM_BACKGROUND, ALARM_APPLICATION]
+
+       ALARM_FACTORY = {
+               ALARM_NONE: _NoneAlarmHandler,
+               ALARM_BACKGROUND: _BACKGROUND_ALARM_FACTORY,
+               ALARM_APPLICATION: _ApplicationAlarmHandler,
+       }
+
+       def __init__(self):
+               self._alarms = {self.ALARM_NONE: _NoneAlarmHandler()}
+               self._currentAlarmType = self.ALARM_NONE
+
+       def load_settings(self, config, sectionName):
+               try:
+                       self._currentAlarmType = config.get(sectionName, "alarm")
+               except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
+                       _moduleLogger.exception("Falling back to old style")
+                       self._currentAlarmType = self.ALARM_BACKGROUND
+               if self._currentAlarmType not in self.ALARM_TYPES:
+                       self._currentAlarmType = self.ALARM_NONE
+
+               self._init_alarm(self._currentAlarmType)
+               if self._currentAlarmType in self._alarms:
+                       self._alarms[self._currentAlarmType].load_settings(config, sectionName)
+                       if not self._alarms[self._currentAlarmType].isEnabled:
+                               _moduleLogger.info("Config file lied, not actually enabled")
+                               self._currentAlarmType = self.ALARM_NONE
+               else:
+                       _moduleLogger.info("Background alerts not supported")
+                       self._currentAlarmType = self.ALARM_NONE
+
+       def save_settings(self, config, sectionName):
+               config.set(sectionName, "alarm", self._currentAlarmType)
+               self._alarms[self._currentAlarmType].save_settings(config, sectionName)
+
+       def apply_settings(self, t, recurrence):
+               self._init_alarm(t)
+               newHandler = self._alarms[t]
+               oldHandler = self._alarms[self._currentAlarmType]
+               if newHandler != oldHandler:
+                       oldHandler.apply_settings(False, 0)
+               newHandler.apply_settings(True, recurrence)
+               self._currentAlarmType = t
+
+       @property
+       def alarmType(self):
+               return self._currentAlarmType
+
+       @property
+       def backgroundNotificationsSupported(self):
+               return self.ALARM_FACTORY[self.ALARM_BACKGROUND] is not None
+
+       @property
+       def applicationNotifySignal(self):
+               self._init_alarm(self.ALARM_APPLICATION)
+               return self._alarms[self.ALARM_APPLICATION].notifySignal
+
+       @property
+       def recurrence(self):
+               return self._alarms[self._currentAlarmType].recurrence
+
+       @property
+       def isEnabled(self):
+               return self._currentAlarmType != self.ALARM_NONE
+
+       def _init_alarm(self, t):
+               if t not in self._alarms and self.ALARM_FACTORY[t] is not None:
+                       self._alarms[t] = self.ALARM_FACTORY[t]()
+
+
 def main():
        logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
        logging.basicConfig(level=logging.DEBUG, format=logFormat)
index 8904ec6..bc6240e 100755 (executable)
@@ -5,6 +5,7 @@ import filecmp
 import ConfigParser
 import pprint
 import logging
+import logging.handlers
 
 import constants
 from backends.gvoice import gvoice
@@ -30,12 +31,22 @@ def get_sms(backend):
 
 def remove_reltime(data):
        for messageData in data["messages"].itervalues():
-               del messageData["relativeStartTime"]
-               del messageData["labels"]
-               del messageData["isRead"]
-               del messageData["isSpam"]
-               del messageData["isTrash"]
-               del messageData["star"]
+               for badPart in [
+                       "relTime",
+                       "relativeStartTime",
+                       "time",
+                       "star",
+                       "isArchived",
+                       "isRead",
+                       "isSpam",
+                       "isTrash",
+                       "labels",
+               ]:
+                       if badPart in messageData:
+                               del messageData[badPart]
+       for globalBad in ["unreadCounts", "totalSize", "resultsPerPage"]:
+               if globalBad in data:
+                       del data[globalBad]
 
 
 def is_type_changed(backend, type, get_material):
@@ -82,7 +93,7 @@ def create_backend(config):
        loggedIn = False
 
        if not loggedIn:
-               loggedIn = backend.is_authed()
+               loggedIn = backend.refresh_account_info() is not None
 
        if not loggedIn:
                import base64
@@ -96,7 +107,7 @@ def create_backend(config):
                                for blob in blobs
                        )
                        username, password = tuple(creds)
-                       loggedIn = backend.login(username, password)
+                       loggedIn = backend.login(username, password) is not None
                except ConfigParser.NoOptionError, e:
                        pass
                except ConfigParser.NoSectionError, e:
@@ -154,7 +165,12 @@ def notify_on_change():
 
 
 if __name__ == "__main__":
-       logging.basicConfig(level=logging.WARNING, filename=constants._notifier_logpath_)
+       logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+       logging.basicConfig(level=logging.DEBUG, format=logFormat)
+       rotating = logging.handlers.RotatingFileHandler(constants._notifier_logpath_, maxBytes=512*1024, backupCount=1)
+       rotating.setFormatter(logging.Formatter(logFormat))
+       root = logging.getLogger()
+       root.addHandler(rotating)
        logging.info("Notifier %s-%s" % (constants.__version__, constants.__build__))
        logging.info("OS: %s" % (os.uname()[0], ))
        logging.info("Kernel: %s (%s) for %s" % os.uname()[2:])
index b82917b..9f8927a 100644 (file)
@@ -21,11 +21,19 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 Filesystem backend for contact support
 """
 
+from __future__ import with_statement
 
 import os
 import csv
 
 
+def try_unicode(s):
+       try:
+               return s.decode("UTF-8")
+       except UnicodeDecodeError:
+               return s
+
+
 class CsvAddressBook(object):
        """
        Currently supported file format
@@ -44,7 +52,7 @@ class CsvAddressBook(object):
        def name(self):
                return self._name
 
-       def update_contacts(self, force = True):
+       def update_account(self, force = True):
                if not force or not self._contacts:
                        return
                self._contacts = dict(
@@ -63,11 +71,12 @@ class CsvAddressBook(object):
 
        def _read_csv(self, csvPath):
                try:
-                       csvReader = iter(csv.reader(open(csvPath, "rU")))
+                       f = open(csvPath, "rU")
+                       csvReader = iter(csv.reader(f))
                except IOError, e:
-                       if e.errno != 2:
-                               raise
-                       return
+                       if e.errno == 2:
+                               return
+                       raise
 
                header = csvReader.next()
                nameColumns, nameFallbacks, phoneColumns = self._guess_columns(header)
@@ -80,7 +89,7 @@ class CsvAddressBook(object):
                                        if len(row[phoneColumn]) == 0:
                                                continue
                                        contactDetails.append({
-                                               "phoneType": phoneType,
+                                               "phoneType": try_unicode(phoneType),
                                                "phoneNumber": row[phoneColumn],
                                        })
                                except IndexError:
@@ -96,6 +105,7 @@ class CsvAddressBook(object):
                                                        break
                                        else:
                                                fullName = "Unknown"
+                               fullName = try_unicode(fullName)
                                yield str(yieldCount), {
                                        "contactId": "%s-%d" % (self._name, yieldCount),
                                        "name": fullName,
index cf7fd6a..17bbc90 100644 (file)
@@ -40,22 +40,31 @@ _moduleLogger = logging.getLogger(__name__)
 
 class GVDialer(object):
 
+       MESSAGE_TEXTS = "Text"
+       MESSAGE_VOICEMAILS = "Voicemail"
+       MESSAGE_ALL = "All"
+
+       HISTORY_RECEIVED = "Received"
+       HISTORY_MISSED = "Missed"
+       HISTORY_PLACED = "Placed"
+       HISTORY_ALL = "All"
+
        def __init__(self, cookieFile = None):
                self._gvoice = gvoice.GVoiceBackend(cookieFile)
+               self._texts = []
+               self._voicemails = []
+               self._received = []
+               self._missed = []
+               self._placed = []
 
        def is_quick_login_possible(self):
                """
-               @returns True then is_authed might be enough to login, else full login is required
+               @returns True then refresh_account_info might be enough to login, else full login is required
                """
                return self._gvoice.is_quick_login_possible()
 
-       def is_authed(self, force = False):
-               """
-               Attempts to detect a current session
-               @note Once logged in try not to reauth more than once a minute.
-               @returns If authenticated
-               """
-               return self._gvoice.is_authed(force)
+       def refresh_account_info(self):
+               return self._gvoice.refresh_account_info()
 
        def login(self, username, password):
                """
@@ -65,6 +74,11 @@ class GVDialer(object):
                return self._gvoice.login(username, password)
 
        def logout(self):
+               self._texts = []
+               self._voicemails = []
+               self._received = []
+               self._missed = []
+               self._placed = []
                return self._gvoice.logout()
 
        def persist(self):
@@ -102,15 +116,14 @@ class GVDialer(object):
        def get_feed(self, feed):
                return self._gvoice.get_feed(feed)
 
-       def download(self, messageId, adir):
+       def download(self, messageId, targetPath):
                """
                Download a voicemail or recorded call MP3 matching the given ``msg``
                which can either be a ``Message`` instance, or a SHA1 identifier. 
-               Saves files to ``adir`` (defaults to current directory). 
                Message hashes can be found in ``self.voicemail().messages`` for example. 
                Returns location of saved file.
                """
-               return self._gvoice.download(messageId, adir)
+               self._gvoice.download(messageId, targetPath)
 
        def is_valid_syntax(self, number):
                """
@@ -144,24 +157,53 @@ class GVDialer(object):
                """
                return self._gvoice.get_callback_number()
 
-       def get_recent(self):
+       def get_call_history(self, historyType):
                """
                @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
                """
-               return list(self._gvoice.get_recent())
+               history = list(self._get_call_history(historyType))
+               history.sort(key=lambda item: item["time"])
+               return history
 
-       def get_contacts(self):
+       def _get_call_history(self, historyType):
                """
-               @returns Fresh dictionary of items
+               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
                """
-               return dict(self._gvoice.get_contacts())
-
-       def get_messages(self):
-               return list(self._get_messages())
+               if historyType in [self.HISTORY_RECEIVED, self.HISTORY_ALL] or not self._received:
+                       self._received = list(self._gvoice.get_received_calls())
+                       for item in self._received:
+                               item["action"] = self.HISTORY_RECEIVED
+               if historyType in [self.HISTORY_MISSED, self.HISTORY_ALL] or not self._missed:
+                       self._missed = list(self._gvoice.get_missed_calls())
+                       for item in self._missed:
+                               item["action"] = self.HISTORY_MISSED
+               if historyType in [self.HISTORY_PLACED, self.HISTORY_ALL] or not self._placed:
+                       self._placed = list(self._gvoice.get_placed_calls())
+                       for item in self._placed:
+                               item["action"] = self.HISTORY_PLACED
+               received = self._received
+               missed = self._missed
+               placed = self._placed
+               for item in received:
+                       yield item
+               for item in missed:
+                       yield item
+               for item in placed:
+                       yield item
+
+       def get_messages(self, messageType):
+               messages = list(self._get_messages(messageType))
+               messages.sort(key=lambda message: message["time"])
+               return messages
+
+       def _get_messages(self, messageType):
+               if messageType in [self.MESSAGE_VOICEMAILS, self.MESSAGE_ALL] or not self._voicemails:
+                       self._voicemails = list(self._gvoice.get_voicemails())
+               if messageType in [self.MESSAGE_TEXTS, self.MESSAGE_ALL] or not self._texts:
+                       self._texts = list(self._gvoice.get_texts())
+               voicemails = self._voicemails
+               smss = self._texts
 
-       def _get_messages(self):
-               voicemails = self._gvoice.get_voicemails()
-               smss = self._gvoice.get_texts()
                conversations = itertools.chain(voicemails, smss)
                for conversation in conversations:
                        messages = conversation.messages
index 3f3cc51..4fef6e8 100644 (file)
@@ -33,7 +33,7 @@ import socket
 
 
 _moduleLogger = logging.getLogger(__name__)
-socket.setdefaulttimeout(45)
+socket.setdefaulttimeout(25)
 
 
 def add_proxy(protocol, url, port):
@@ -48,6 +48,7 @@ def add_proxy(protocol, url, port):
 class MozillaEmulator(object):
 
        USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729)'
+       #USER_AGENT = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16"
 
        def __init__(self, trycount = 1):
                """Create a new MozillaEmulator object.
index 3cfd53f..b0825ef 100755 (executable)
@@ -170,7 +170,6 @@ class GVoiceBackend(object):
 
                SECURE_URL_BASE = "https://www.google.com/voice/"
                SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
-               self._forwardURL = SECURE_MOBILE_URL_BASE + "phones"
                self._tokenURL = SECURE_URL_BASE + "m"
                self._callUrl = SECURE_URL_BASE + "call/connect"
                self._callCancelURL = SECURE_URL_BASE + "call/cancel"
@@ -211,9 +210,6 @@ class GVoiceBackend(object):
                self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
 
                self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
-               self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
-               self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
-               self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
 
                self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
                self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
@@ -230,32 +226,21 @@ class GVoiceBackend(object):
 
        def is_quick_login_possible(self):
                """
-               @returns True then is_authed might be enough to login, else full login is required
+               @returns True then refresh_account_info might be enough to login, else full login is required
                """
                return self._loadedFromCookies or 0.0 < self._lastAuthed
 
-       def is_authed(self, force = False):
-               """
-               Attempts to detect a current session
-               @note Once logged in try not to reauth more than once a minute.
-               @returns If authenticated
-               @blocks
-               """
-               isRecentledAuthed = (time.time() - self._lastAuthed) < 120
-               isPreviouslyAuthed = self._token is not None
-               if isRecentledAuthed and isPreviouslyAuthed and not force:
-                       return True
-
+       def refresh_account_info(self):
                try:
-                       page = self._get_page(self._forwardURL)
-                       self._grab_account_info(page)
+                       page = self._get_page(self._JSON_CONTACTS_URL)
+                       accountData = self._grab_account_info(page)
                except Exception, e:
                        _moduleLogger.exception(str(e))
-                       return False
+                       return None
 
                self._browser.save_cookies()
                self._lastAuthed = time.time()
-               return True
+               return accountData
 
        def _get_token(self):
                tokenPage = self._get_page(self._tokenURL)
@@ -277,7 +262,7 @@ class GVoiceBackend(object):
                        "btmpl": "mobile",
                        "PersistentCookie": "yes",
                        "GALX": token,
-                       "continue": self._forwardURL,
+                       "continue": self._JSON_CONTACTS_URL,
                }
 
                loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
@@ -294,19 +279,19 @@ class GVoiceBackend(object):
                loginSuccessOrFailurePage = self._login(username, password, galxToken)
 
                try:
-                       self._grab_account_info(loginSuccessOrFailurePage)
+                       accountData = self._grab_account_info(loginSuccessOrFailurePage)
                except Exception, e:
                        # Retry in case the redirect failed
-                       # luckily is_authed does everything we need for a retry
-                       loggedIn = self.is_authed(True)
-                       if not loggedIn:
+                       # luckily refresh_account_info does everything we need for a retry
+                       accountData = self.refresh_account_info()
+                       if accountData is None:
                                _moduleLogger.exception(str(e))
-                               return False
+                               return None
                        _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
 
                self._browser.save_cookies()
                self._lastAuthed = time.time()
-               return True
+               return accountData
 
        def persist(self):
                self._browser.save_cookies()
@@ -321,6 +306,7 @@ class GVoiceBackend(object):
                self._browser.save_cookies()
                self._token = None
                self._lastAuthed = 0.0
+               self._callbackNumbers = {}
 
        def is_dnd(self):
                """
@@ -429,20 +415,21 @@ class GVoiceBackend(object):
 
                return json
 
-       def download(self, messageId, adir):
+       def recording_url(self, messageId):
+               url = self._downloadVoicemailURL+messageId
+               return url
+
+       def download(self, messageId, targetPath):
                """
                Download a voicemail or recorded call MP3 matching the given ``msg``
                which can either be a ``Message`` instance, or a SHA1 identifier. 
-               Saves files to ``adir`` (defaults to current directory). 
                Message hashes can be found in ``self.voicemail().messages`` for example. 
                @returns location of saved file.
                @blocks
                """
-               page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
-               fn = os.path.join(adir, '%s.mp3' % messageId)
-               with open(fn, 'wb') as fo:
+               page = self._get_page(self.recording_url(messageId))
+               with open(targetPath, 'wb') as fo:
                        fo.write(page)
-               return fn
 
        def is_valid_syntax(self, number):
                """
@@ -478,28 +465,26 @@ class GVoiceBackend(object):
                """
                return self._callbackNumber
 
-       def get_recent(self):
+       def get_received_calls(self):
                """
                @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
                @blocks
                """
-               recentPages = [
-                       (action, self._get_page(url))
-                       for action, url in (
-                               ("Received", self._XML_RECEIVED_URL),
-                               ("Missed", self._XML_MISSED_URL),
-                               ("Placed", self._XML_PLACED_URL),
-                       )
-               ]
-               return self._parse_recent(recentPages)
+               return self._parse_recent(self._get_page(self._XML_RECEIVED_URL))
+
+       def get_missed_calls(self):
+               """
+               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+               @blocks
+               """
+               return self._parse_recent(self._get_page(self._XML_MISSED_URL))
 
-       def get_contacts(self):
+       def get_placed_calls(self):
                """
-               @returns Iterable of (contact id, contact name)
+               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
                @blocks
                """
-               page = self._get_page(self._JSON_CONTACTS_URL)
-               return self._process_contacts(page)
+               return self._parse_recent(self._get_page(self._XML_PLACED_URL))
 
        def get_csv_contacts(self):
                data = {
@@ -578,46 +563,25 @@ class GVoiceBackend(object):
                return flatHtml
 
        def _grab_account_info(self, page):
-               tokenGroup = self._tokenRe.search(page)
-               if tokenGroup is None:
-                       raise RuntimeError("Could not extract authentication token from GoogleVoice")
-               self._token = tokenGroup.group(1)
-
-               anGroup = self._accountNumRe.search(page)
-               if anGroup is not None:
-                       self._accountNum = anGroup.group(1)
-               else:
-                       _moduleLogger.debug("Could not extract account number from GoogleVoice")
-
-               self._callbackNumbers = {}
-               for match in self._callbackRe.finditer(page):
-                       callbackNumber = match.group(2)
-                       callbackName = match.group(1)
-                       self._callbackNumbers[callbackNumber] = callbackName
+               accountData = parse_json(page)
+               self._token = accountData["r"]
+               self._accountNum = accountData["number"]["raw"]
+               for callback in accountData["phones"].itervalues():
+                       self._callbackNumbers[callback["phoneNumber"]] = callback["name"]
                if len(self._callbackNumbers) == 0:
                        _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
+               return accountData
 
        def _send_validation(self, number):
                if not self.is_valid_syntax(number):
                        raise ValueError('Number is not valid: "%s"' % number)
-               elif not self.is_authed():
-                       raise RuntimeError("Not Authenticated")
                return number
 
-       def _parse_recent(self, recentPages):
-               for action, flatXml in recentPages:
-                       allRecentHtml = self._grab_html(flatXml)
-                       allRecentData = self._parse_history(allRecentHtml)
-                       for recentCallData in allRecentData:
-                               recentCallData["action"] = action
-                               yield recentCallData
-
-       def _process_contacts(self, page):
-               accountData = parse_json(page)
-               for contactId, contactDetails in accountData["contacts"].iteritems():
-                       # A zero contact id is the catch all for unknown contacts
-                       if contactId != "0":
-                               yield contactId, contactDetails
+       def _parse_recent(self, recentPage):
+               allRecentHtml = self._grab_html(recentPage)
+               allRecentData = self._parse_history(allRecentHtml)
+               for recentCallData in allRecentData:
+                       yield recentCallData
 
        def _parse_history(self, historyHtml):
                splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
@@ -986,7 +950,6 @@ def grab_debug_info(username, password):
        browser = backend._browser
 
        _TEST_WEBPAGES = [
-               ("forward", backend._forwardURL),
                ("token", backend._tokenURL),
                ("login", backend._loginURL),
                ("isdnd", backend._isDndURL),
@@ -1029,8 +992,8 @@ def grab_debug_info(username, password):
                backend._grab_account_info(loginSuccessOrFailurePage)
        except Exception:
                # Retry in case the redirect failed
-               # luckily is_authed does everything we need for a retry
-               loggedIn = backend.is_authed(True)
+               # luckily refresh_account_info does everything we need for a retry
+               loggedIn = backend.refresh_account_info() is not None
                if not loggedIn:
                        raise
 
@@ -1056,6 +1019,21 @@ def grab_debug_info(username, password):
                )
 
 
+def grab_voicemails(username, password):
+       cookieFile = os.path.join(".", "raw_cookies.txt")
+       try:
+               os.remove(cookieFile)
+       except OSError:
+               pass
+
+       backend = GVoiceBackend(cookieFile)
+       backend.login(username, password)
+       voicemails = list(backend.get_voicemails())
+       for voicemail in voicemails:
+               print voicemail.id
+               backend.download(voicemail.id, ".")
+
+
 def main():
        import sys
        logging.basicConfig(level=logging.DEBUG)
@@ -1065,6 +1043,7 @@ def main():
                password = args[2]
 
        grab_debug_info(username, password)
+       grab_voicemails(username, password)
 
 
 if __name__ == "__main__":
index fcd3a1d..ebaa932 100644 (file)
@@ -26,7 +26,7 @@ class NullAddressBook(object):
        def name(self):
                return "None"
 
-       def update_contacts(self, force = True):
+       def update_account(self, force = True):
                pass
 
        def get_contacts(self):
diff --git a/src/backends/qt_backend.py b/src/backends/qt_backend.py
new file mode 100644 (file)
index 0000000..a509e1d
--- /dev/null
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import util.qt_compat as qt_compat
+if qt_compat.USES_PYSIDE:
+       import QtMobility.Contacts as _QtContacts
+       QtContacts = _QtContacts
+else:
+       QtContacts = None
+
+import null_backend
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class QtContactsAddressBook(object):
+
+       def __init__(self, name, uri):
+               self._name = name
+               self._uri = uri
+               self._manager = QtContacts.QContactManager.fromUri(uri)
+               self._contacts = None
+
+       @property
+       def name(self):
+               return self._name
+
+       @property
+       def error(self):
+               return self._manager.error()
+
+       def update_account(self, force = True):
+               if not force and self._contacts is not None:
+                       return
+               self._contacts = dict(self._get_contacts())
+
+       def get_contacts(self):
+               if self._contacts is None:
+                       self._contacts = dict(self._get_contacts())
+               return self._contacts
+
+       def _get_contacts(self):
+               contacts = self._manager.contacts()
+               for contact in contacts:
+                       contactId = contact.localId()
+                       contactName = contact.displayLabel()
+                       phoneDetails = contact.details(QtContacts.QContactPhoneNumber().DefinitionName)
+                       phones = [{"phoneType": "Phone", "phoneNumber": phone.value(QtContacts.QContactPhoneNumber().FieldNumber)} for phone in phoneDetails]
+                       contactDetails = phones
+                       if 0 < len(contactDetails):
+                               yield str(contactId), {
+                                       "contactId": str(contactId),
+                                       "name": contactName,
+                                       "numbers": contactDetails,
+                               }
+
+
+class _QtContactsAddressBookFactory(object):
+
+       def __init__(self):
+               self._availableManagers = {}
+
+               availableMgrs = QtContacts.QContactManager.availableManagers()
+               availableMgrs.remove("invalid")
+               for managerName in availableMgrs:
+                       params = {}
+                       managerUri = QtContacts.QContactManager.buildUri(managerName, params)
+                       self._availableManagers[managerName] =  managerUri
+
+       def get_addressbooks(self):
+               for name, uri in self._availableManagers.iteritems():
+                       book = QtContactsAddressBook(name, uri)
+                       if book.error:
+                               _moduleLogger.info("Could not load %r due to %r" % (name, book.error))
+                       else:
+                               yield book
+
+
+class _EmptyAddressBookFactory(object):
+
+       def get_addressbooks(self):
+               if False:
+                       yield None
+
+
+if QtContacts is not None:
+       QtContactsAddressBookFactory = _QtContactsAddressBookFactory
+else:
+       QtContactsAddressBookFactory = _EmptyAddressBookFactory
+       _moduleLogger.info("QtContacts support not available")
+
+
+if __name__ == "__main__":
+       factory = QtContactsAddressBookFactory()
+       books = factory.get_addressbooks()
+       for book in books:
+               print book.name
+               print book.get_contacts()
diff --git a/src/call_handler.py b/src/call_handler.py
new file mode 100644 (file)
index 0000000..9b9c47d
--- /dev/null
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+import dbus
+try:
+       import telepathy as _telepathy
+       import util.tp_utils as telepathy_utils
+       telepathy = _telepathy
+except ImportError:
+       telepathy = None
+
+import util.misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class _FakeSignaller(object):
+
+       def start(self):
+               pass
+
+       def stop(self):
+               pass
+
+
+class _MissedCallWatcher(QtCore.QObject):
+
+       callMissed = qt_compat.Signal()
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+               self._isStarted = False
+               self._isSupported = True
+
+               self._newChannelSignaller = telepathy_utils.NewChannelSignaller(self._on_new_channel)
+               self._outstandingRequests = []
+
+       @property
+       def isSupported(self):
+               return self._isSupported
+
+       @property
+       def isStarted(self):
+               return self._isStarted
+
+       def start(self):
+               if self._isStarted:
+                       _moduleLogger.info("voicemail monitor already started")
+                       return
+               try:
+                       self._newChannelSignaller.start()
+               except RuntimeError:
+                       _moduleLogger.exception("Missed call detection not supported")
+                       self._newChannelSignaller = _FakeSignaller()
+                       self._isSupported = False
+               self._isStarted = True
+
+       def stop(self):
+               if not self._isStarted:
+                       _moduleLogger.info("voicemail monitor stopped without starting")
+                       return
+               _moduleLogger.info("Stopping voicemail refresh")
+               self._newChannelSignaller.stop()
+
+               # I don't want to trust whether the cancel happens within the current
+               # callback or not which could be the deciding factor between invalid
+               # iterators or infinite loops
+               localRequests = [r for r in self._outstandingRequests]
+               for request in localRequests:
+                       localRequests.cancel()
+
+               self._isStarted = False
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_new_channel(self, bus, serviceName, connObjectPath, channelObjectPath, channelType):
+               if channelType != telepathy.interfaces.CHANNEL_TYPE_STREAMED_MEDIA:
+                       return
+
+               conn = telepathy.client.Connection(serviceName, connObjectPath)
+               try:
+                       chan = telepathy.client.Channel(serviceName, channelObjectPath)
+               except dbus.exceptions.UnknownMethodException:
+                       _moduleLogger.exception("Client might not have implemented a deprecated method")
+                       return
+               missDetection = telepathy_utils.WasMissedCall(
+                       bus, conn, chan, self._on_missed_call, self._on_error_for_missed
+               )
+               self._outstandingRequests.append(missDetection)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_missed_call(self, missDetection):
+               _moduleLogger.info("Missed a call")
+               self.callMissed.emit()
+               self._outstandingRequests.remove(missDetection)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_error_for_missed(self, missDetection, reason):
+               _moduleLogger.debug("Error: %r claims %r" % (missDetection, reason))
+               self._outstandingRequests.remove(missDetection)
+
+
+class _DummyMissedCallWatcher(QtCore.QObject):
+
+       callMissed = qt_compat.Signal()
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+               self._isStarted = False
+
+       @property
+       def isSupported(self):
+               return False
+
+       @property
+       def isStarted(self):
+               return self._isStarted
+
+       def start(self):
+               self._isStarted = True
+
+       def stop(self):
+               if not self._isStarted:
+                       _moduleLogger.info("voicemail monitor stopped without starting")
+                       return
+               _moduleLogger.info("Stopping voicemail refresh")
+               self._isStarted = False
+
+
+if telepathy is not None:
+       MissedCallWatcher = _MissedCallWatcher
+else:
+       MissedCallWatcher = _DummyMissedCallWatcher
+
+
+if __name__ == "__main__":
+       pass
+
index 2b571e7..76d11b4 100644 (file)
@@ -2,7 +2,7 @@ import os
 
 __pretty_app_name__ = "DialCentral"
 __app_name__ = "dialcentral"
-__version__ = "1.2.33"
+__version__ = "1.3.0"
 __build__ = 0
 __app_magic__ = 0xdeadbeef
 _data_path_ = os.path.join(os.path.expanduser("~"), ".%s" % __app_name__)
index e96e2f5..86a0db1 100755 (executable)
@@ -9,31 +9,14 @@ published by the Free Software Foundation.
 """
 
 
-import os
 import sys
-import logging
 
 
-_moduleLogger = logging.getLogger(__name__)
 sys.path.append("/opt/dialcentral/lib")
 
 
-import constants
 import dialcentral_qt
 
 
 if __name__ == "__main__":
-       try:
-               os.makedirs(constants._data_path_)
-       except OSError, e:
-               if e.errno != 17:
-                       raise
-
-       logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
-       logging.basicConfig(level=logging.DEBUG, filename=constants._user_logpath_, format=logFormat)
-       _moduleLogger.info("%s %s-%s" % (constants.__app_name__, constants.__version__, constants.__build__))
-       _moduleLogger.info("OS: %s" % (os.uname()[0], ))
-       _moduleLogger.info("Kernel: %s (%s) for %s" % os.uname()[2:])
-       _moduleLogger.info("Hostname: %s" % os.uname()[1])
-
        dialcentral_qt.run()
index 8523ad5..e39d553 100755 (executable)
@@ -8,11 +8,14 @@ import base64
 import ConfigParser
 import functools
 import logging
+import logging.handlers
 
-from PyQt4 import QtGui
-from PyQt4 import QtCore
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
 
 import constants
+import alarm_handler
 from util import qtpie
 from util import qwrappers
 from util import qui_utils
@@ -24,29 +27,6 @@ import session
 _moduleLogger = logging.getLogger(__name__)
 
 
-class LedWrapper(object):
-
-       def __init__(self):
-               self._ledHandler = None
-               self._init = False
-
-       def off(self):
-               self._lazy_init()
-               if self._ledHandler is not None:
-                       self._ledHandler.off()
-
-       def _lazy_init(self):
-               if self._init:
-                       return
-               self._init = True
-               try:
-                       import led_handler
-                       self._ledHandler = led_handler.LedHandler()
-               except Exception, e:
-                       _moduleLogger.exception('Unable to initialize LED Handling: "%s"' % str(e))
-                       self._ledHandler = None
-
-
 class Dialcentral(qwrappers.ApplicationWrapper):
 
        _DATA_PATHS = [
@@ -57,24 +37,13 @@ class Dialcentral(qwrappers.ApplicationWrapper):
        def __init__(self, app):
                self._dataPath = None
                self._aboutDialog = None
-               self._ledHandler = LedWrapper()
                self.notifyOnMissed = False
                self.notifyOnVoicemail = False
                self.notifyOnSms = False
 
-               try:
-                       import alarm_handler
-                       if alarm_handler.AlarmHandler is not alarm_handler._NoneAlarmHandler:
-                               self._alarmHandler = alarm_handler.AlarmHandler()
-                       else:
-                               self._alarmHandler = None
-               except (ImportError, OSError):
-                       self._alarmHandler = None
-               except Exception:
-                       _moduleLogger.exception("Notification failure")
-                       self._alarmHandler = None
-               if self._alarmHandler is None:
-                       _moduleLogger.info("No notification support")
+               self._streamHandler = None
+               self._ledHandler = None
+               self._alarmHandler = alarm_handler.AlarmHandler()
 
                qwrappers.ApplicationWrapper.__init__(self, app, constants)
 
@@ -94,88 +63,12 @@ class Dialcentral(qwrappers.ApplicationWrapper):
                except Exception:
                        _moduleLogger.exception("Unknown loading error")
 
-               blobs = "", ""
-               isFullscreen = False
-               isPortrait = qui_utils.screen_orientation() == QtCore.Qt.Vertical
-               tabIndex = 0
-               try:
-                       blobs = [
-                               config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
-                               for i in xrange(len(self._mainWindow.get_default_credentials()))
-                       ]
-                       isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
-                       tabIndex = config.getint(constants.__pretty_app_name__, "tab")
-                       isPortrait = config.getboolean(constants.__pretty_app_name__, "portrait")
-               except ConfigParser.NoOptionError, e:
-                       _moduleLogger.info(
-                               "Settings file %s is missing option %s" % (
-                                       constants._user_settings_,
-                                       e.option,
-                               ),
-                       )
-               except ConfigParser.NoSectionError, e:
-                       _moduleLogger.info(
-                               "Settings file %s is missing section %s" % (
-                                       constants._user_settings_,
-                                       e.section,
-                               ),
-                       )
-               except Exception:
-                       _moduleLogger.exception("Unknown loading error")
-
-               if self._alarmHandler is not None:
-                       try:
-                               self._alarmHandler.load_settings(config, "alarm")
-                               self.notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
-                               self.notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
-                               self.notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
-                       except ConfigParser.NoOptionError, e:
-                               _moduleLogger.info(
-                                       "Settings file %s is missing option %s" % (
-                                               constants._user_settings_,
-                                               e.option,
-                                       ),
-                               )
-                       except ConfigParser.NoSectionError, e:
-                               _moduleLogger.info(
-                                       "Settings file %s is missing section %s" % (
-                                               constants._user_settings_,
-                                               e.section,
-                                       ),
-                               )
-                       except Exception:
-                               _moduleLogger.exception("Unknown loading error")
-
-               creds = (
-                       base64.b64decode(blob)
-                       for blob in blobs
-               )
-               self._mainWindow.set_default_credentials(*creds)
-               self._fullscreenAction.setChecked(isFullscreen)
-               self._orientationAction.setChecked(isPortrait)
-               self._mainWindow.set_current_tab(tabIndex)
                self._mainWindow.load_settings(config)
 
        def save_settings(self):
                _moduleLogger.info("Saving settings")
                config = ConfigParser.SafeConfigParser()
 
-               config.add_section(constants.__pretty_app_name__)
-               config.set(constants.__pretty_app_name__, "tab", str(self._mainWindow.get_current_tab()))
-               config.set(constants.__pretty_app_name__, "fullscreen", str(self._fullscreenAction.isChecked()))
-               config.set(constants.__pretty_app_name__, "portrait", str(self._orientationAction.isChecked()))
-               for i, value in enumerate(self._mainWindow.get_default_credentials()):
-                       blob = base64.b64encode(value)
-                       config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
-
-               if self._alarmHandler is not None:
-                       config.add_section("alarm")
-                       self._alarmHandler.save_settings(config, "alarm")
-               config.add_section("2 - Account Info")
-               config.set("2 - Account Info", "notifyOnMissed", repr(self.notifyOnMissed))
-               config.set("2 - Account Info", "notifyOnVoicemail", repr(self.notifyOnVoicemail))
-               config.set("2 - Account Info", "notifyOnSms", repr(self.notifyOnSms))
-
                self._mainWindow.save_settings(config)
 
                with open(constants._user_settings_, "wb") as configFile:
@@ -193,6 +86,17 @@ class Dialcentral(qwrappers.ApplicationWrapper):
                else:
                        return None
 
+       def get_resource(self, name):
+               if self._dataPath is None:
+                       for path in self._DATA_PATHS:
+                               if os.path.exists(os.path.join(path, name)):
+                                       self._dataPath = path
+                                       break
+               if self._dataPath is not None:
+                       return os.path.join(self._dataPath, name)
+               else:
+                       return None
+
        def _close_windows(self):
                qwrappers.ApplicationWrapper._close_windows(self)
                if self._aboutDialog  is not None:
@@ -203,18 +107,28 @@ class Dialcentral(qwrappers.ApplicationWrapper):
                return os.path.join(constants._data_path_, "contacts")
 
        @property
+       def streamHandler(self):
+               if self._streamHandler is None:
+                       import stream_handler
+                       self._streamHandler = stream_handler.StreamHandler()
+               return self._streamHandler
+
+       @property
        def alarmHandler(self):
                return self._alarmHandler
 
        @property
        def ledHandler(self):
+               if self._ledHandler is None:
+                       import led_handler
+                       self._ledHandler = led_handler.LedHandler()
                return self._ledHandler
 
        def _new_main_window(self):
                return MainWindow(None, self)
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
        def _on_about(self, checked = True):
                with qui_utils.notify_error(self._errorLog):
@@ -342,7 +256,7 @@ class MainWindow(qwrappers.WindowWrapper):
        def __init__(self, parent, app):
                qwrappers.WindowWrapper.__init__(self, parent, app)
                self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
-               #self._freezer = qwrappers.AutoFreezeWindowFeature(self._app, self._window)
+               self._window.resized.connect(self._on_window_resized)
                self._errorLog = self._app.errorLog
 
                self._session = session.Session(self._errorLog, constants._data_path_)
@@ -350,6 +264,15 @@ class MainWindow(qwrappers.WindowWrapper):
                self._session.loggedIn.connect(self._on_login)
                self._session.loggedOut.connect(self._on_logout)
                self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
+               self._session.newMessages.connect(self._on_new_message_alert)
+               self._app.alarmHandler.applicationNotifySignal.connect(self._on_app_alert)
+               self._voicemailRefreshDelay = QtCore.QTimer()
+               self._voicemailRefreshDelay.setInterval(30 * 1000)
+               self._voicemailRefreshDelay.timeout.connect(self._on_call_missed)
+               self._voicemailRefreshDelay.setSingleShot(True)
+               self._callHandler = None
+               self._updateVoicemailOnMissedCall = False
+
                self._defaultCredentials = "", ""
                self._curentCredentials = "", ""
                self._currentTab = 0
@@ -394,35 +317,41 @@ class MainWindow(qwrappers.WindowWrapper):
 
                self._layout.addWidget(self._tabWidget)
 
-               self._loginTabAction = QtGui.QAction(None)
-               self._loginTabAction.setText("Login")
-               self._loginTabAction.triggered.connect(self._on_login_requested)
+               self._loginAction = QtGui.QAction(None)
+               self._loginAction.setText("Login")
+               self._loginAction.triggered.connect(self._on_login_requested)
 
-               self._importTabAction = QtGui.QAction(None)
-               self._importTabAction.setText("Import")
-               self._importTabAction.triggered.connect(self._on_import)
+               self._importAction = QtGui.QAction(None)
+               self._importAction.setText("Import")
+               self._importAction.triggered.connect(self._on_import)
 
-               self._accountTabAction = QtGui.QAction(None)
-               self._accountTabAction.setText("Account")
-               self._accountTabAction.triggered.connect(self._on_account)
+               self._accountAction = QtGui.QAction(None)
+               self._accountAction.setText("Account")
+               self._accountAction.triggered.connect(self._on_account)
+
+               self._refreshConnectionAction = QtGui.QAction(None)
+               self._refreshConnectionAction.setText("Refresh Connection")
+               self._refreshConnectionAction.setShortcut(QtGui.QKeySequence("CTRL+a"))
+               self._refreshConnectionAction.triggered.connect(self._on_refresh_connection)
 
                self._refreshTabAction = QtGui.QAction(None)
-               self._refreshTabAction.setText("Refresh")
+               self._refreshTabAction.setText("Refresh Tab")
                self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
                self._refreshTabAction.triggered.connect(self._on_refresh)
 
                fileMenu = self._window.menuBar().addMenu("&File")
-               fileMenu.addAction(self._loginTabAction)
+               fileMenu.addAction(self._loginAction)
                fileMenu.addAction(self._refreshTabAction)
+               fileMenu.addAction(self._refreshConnectionAction)
 
                toolsMenu = self._window.menuBar().addMenu("&Tools")
-               toolsMenu.addAction(self._accountTabAction)
-               toolsMenu.addAction(self._importTabAction)
+               toolsMenu.addAction(self._accountAction)
+               toolsMenu.addAction(self._importAction)
                toolsMenu.addAction(self._app.aboutAction)
 
                self._initialize_tab(self._tabWidget.currentIndex())
                self.set_fullscreen(self._app.fullscreenAction.isChecked())
-               self.set_orientation(self._app.orientationAction.isChecked())
+               self.update_orientation(self._app.orientation)
 
        def set_default_credentials(self, username, password):
                self._defaultCredentials = username, password
@@ -471,6 +400,67 @@ class MainWindow(qwrappers.WindowWrapper):
                self._tabWidget.setCurrentIndex(tabIndex)
 
        def load_settings(self, config):
+               blobs = "", ""
+               isFullscreen = False
+               orientation = self._app.orientation
+               tabIndex = 0
+               try:
+                       blobs = [
+                               config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+                               for i in xrange(len(self.get_default_credentials()))
+                       ]
+                       isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
+                       tabIndex = config.getint(constants.__pretty_app_name__, "tab")
+                       orientation = config.get(constants.__pretty_app_name__, "orientation")
+               except ConfigParser.NoOptionError, e:
+                       _moduleLogger.info(
+                               "Settings file %s is missing option %s" % (
+                                       constants._user_settings_,
+                                       e.option,
+                               ),
+                       )
+               except ConfigParser.NoSectionError, e:
+                       _moduleLogger.info(
+                               "Settings file %s is missing section %s" % (
+                                       constants._user_settings_,
+                                       e.section,
+                               ),
+                       )
+               except Exception:
+                       _moduleLogger.exception("Unknown loading error")
+
+               try:
+                       self._app.alarmHandler.load_settings(config, "alarm")
+                       self._app.notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
+                       self._app.notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
+                       self._app.notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
+                       self._updateVoicemailOnMissedCall = config.getboolean("2 - Account Info", "updateVoicemailOnMissedCall")
+               except ConfigParser.NoOptionError, e:
+                       _moduleLogger.info(
+                               "Settings file %s is missing option %s" % (
+                                       constants._user_settings_,
+                                       e.option,
+                               ),
+                       )
+               except ConfigParser.NoSectionError, e:
+                       _moduleLogger.info(
+                               "Settings file %s is missing section %s" % (
+                                       constants._user_settings_,
+                                       e.section,
+                               ),
+                       )
+               except Exception:
+                       _moduleLogger.exception("Unknown loading error")
+
+               creds = (
+                       base64.b64decode(blob)
+                       for blob in blobs
+               )
+               self.set_default_credentials(*creds)
+               self._app.fullscreenAction.setChecked(isFullscreen)
+               self._app.set_orientation(orientation)
+               self.set_current_tab(tabIndex)
+
                backendId = 2 # For backwards compatibility
                for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
                        sectionName = "%s - %s" % (backendId, tabTitle)
@@ -501,6 +491,22 @@ class MainWindow(qwrappers.WindowWrapper):
                        self._tabsContents[tabIndex].set_settings(settings)
 
        def save_settings(self, config):
+               config.add_section(constants.__pretty_app_name__)
+               config.set(constants.__pretty_app_name__, "tab", str(self.get_current_tab()))
+               config.set(constants.__pretty_app_name__, "fullscreen", str(self._app.fullscreenAction.isChecked()))
+               config.set(constants.__pretty_app_name__, "orientation", str(self._app.orientation))
+               for i, value in enumerate(self.get_default_credentials()):
+                       blob = base64.b64encode(value)
+                       config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
+
+               config.add_section("alarm")
+               self._app.alarmHandler.save_settings(config, "alarm")
+               config.add_section("2 - Account Info")
+               config.set("2 - Account Info", "notifyOnMissed", repr(self._app.notifyOnMissed))
+               config.set("2 - Account Info", "notifyOnVoicemail", repr(self._app.notifyOnVoicemail))
+               config.set("2 - Account Info", "notifyOnSms", repr(self._app.notifyOnSms))
+               config.set("2 - Account Info", "updateVoicemailOnMissedCall", repr(self._updateVoicemailOnMissedCall))
+
                backendId = 2 # For backwards compatibility
                for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
                        sectionName = "%s - %s" % (backendId, tabTitle)
@@ -509,12 +515,13 @@ class MainWindow(qwrappers.WindowWrapper):
                        for settingName, settingValue in tabSettings.iteritems():
                                config.set(sectionName, settingName, settingValue)
 
-       def set_orientation(self, isPortrait):
-               qwrappers.WindowWrapper.set_orientation(self, isPortrait)
-               if isPortrait:
-                       self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
-               else:
+       def update_orientation(self, orientation):
+               qwrappers.WindowWrapper.update_orientation(self, orientation)
+               windowOrientation = self.idealWindowOrientation
+               if windowOrientation == QtCore.Qt.Horizontal:
                        self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
+               else:
+                       self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
 
        def _initialize_tab(self, index):
                assert index < self.MAX_TABS, "Invalid tab"
@@ -539,15 +546,21 @@ class MainWindow(qwrappers.WindowWrapper):
        def _show_account_dialog(self):
                if self._accountDialog is None:
                        import dialogs
-                       self._accountDialog = dialogs.AccountDialog(self._app)
-                       if self._app.alarmHandler is None:
-                               self._accountDialog.setIfNotificationsSupported(False)
-               if self._app.alarmHandler is not None:
-                       self._accountDialog.notifications = self._app.alarmHandler.isEnabled
-                       self._accountDialog.notificationTime = self._app.alarmHandler.recurrence
-                       self._accountDialog.notifyOnMissed = self._app.notifyOnMissed
-                       self._accountDialog.notifyOnVoicemail = self._app.notifyOnVoicemail
-                       self._accountDialog.notifyOnSms = self._app.notifyOnSms
+                       self._accountDialog = dialogs.AccountDialog(self._window, self._app, self._app.errorLog)
+                       self._accountDialog.setIfNotificationsSupported(self._app.alarmHandler.backgroundNotificationsSupported)
+                       self._accountDialog.settingsApproved.connect(self._on_settings_approved)
+
+               if self._callHandler is None or not self._callHandler.isSupported:
+                       self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED
+               elif self._updateVoicemailOnMissedCall:
+                       self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_ENABLED
+               else:
+                       self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_DISABLED
+               self._accountDialog.notifications = self._app.alarmHandler.alarmType
+               self._accountDialog.notificationTime = self._app.alarmHandler.recurrence
+               self._accountDialog.notifyOnMissed = self._app.notifyOnMissed
+               self._accountDialog.notifyOnVoicemail = self._app.notifyOnVoicemail
+               self._accountDialog.notifyOnSms = self._app.notifyOnSms
                self._accountDialog.set_callbacks(
                        self._session.get_callback_numbers(), self._session.get_callback_number()
                )
@@ -555,35 +568,80 @@ class MainWindow(qwrappers.WindowWrapper):
                if not accountNumberToDisplay:
                        accountNumberToDisplay = "Not Available (%s)" % self._session.state
                self._accountDialog.set_account_number(accountNumberToDisplay)
-               response = self._accountDialog.run(self.window)
-               if response == QtGui.QDialog.Accepted:
-                       if self._accountDialog.doClear:
-                               self._session.logout_and_clear()
-                               self._defaultCredentials = "", ""
-                               self._curentCredentials = "", ""
-                               for tab in self._tabsContents:
-                                       tab.disable()
-                       else:
-                               callbackNumber = self._accountDialog.selectedCallback
-                               self._session.set_callback_number(callbackNumber)
-                       if self._app.alarmHandler is not None:
-                               self._app.alarmHandler.apply_settings(self._accountDialog.notifications, self._accountDialog.notificationTime)
-                               self._app.notifyOnMissed = self._accountDialog.notifyOnMissed
-                               self._app.notifyOnVoicemail = self._accountDialog.notifyOnVoicemail
-                               self._app.notifyOnSms = self._accountDialog.notifyOnSms
-                               self._app.save_settings()
-               elif response == QtGui.QDialog.Rejected:
-                       _moduleLogger.info("Cancelled")
+               self._accountDialog.orientation = self._app.orientation
+
+               self._accountDialog.run()
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_settings_approved(self):
+               if self._accountDialog.doClear:
+                       self._session.logout_and_clear()
+                       self._defaultCredentials = "", ""
+                       self._curentCredentials = "", ""
+                       for tab in self._tabsContents:
+                               tab.disable()
+               else:
+                       callbackNumber = self._accountDialog.selectedCallback
+                       self._session.set_callback_number(callbackNumber)
+
+               if self._callHandler is None or self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_DISABLEDD:
+                       pass
+               elif self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_ENABLED:
+                       self._updateVoicemailOnMissedCall = True
+                       self._callHandler.start()
                else:
-                       _moduleLogger.info("Unknown response")
+                       self._updateVoicemailOnMissedCall = False
+                       self._callHandler.stop()
+               if (
+                       self._accountDialog.notifyOnMissed or
+                       self._accountDialog.notifyOnVoicemail or
+                       self._accountDialog.notifyOnSms
+               ):
+                       notifications = self._accountDialog.notifications
+               else:
+                       notifications = self._accountDialog.ALARM_NONE
+               self._app.alarmHandler.apply_settings(notifications, self._accountDialog.notificationTime)
+
+               self._app.notifyOnMissed = self._accountDialog.notifyOnMissed
+               self._app.notifyOnVoicemail = self._accountDialog.notifyOnVoicemail
+               self._app.notifyOnSms = self._accountDialog.notifyOnSms
+               self._app.set_orientation(self._accountDialog.orientation)
+               self._app.save_settings()
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_window_resized(self):
+               with qui_utils.notify_error(self._app.errorLog):
+                       windowOrientation = self.idealWindowOrientation
+                       if windowOrientation == QtCore.Qt.Horizontal:
+                               self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
+                       else:
+                               self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_new_message_alert(self):
+               with qui_utils.notify_error(self._errorLog):
+                       if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
+                               if self._currentTab == self.MESSAGES_TAB or not self._app.ledHandler.isReal:
+                                       self._errorLog.push_message("New messages available")
+                               else:
+                                       self._app.ledHandler.on()
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_call_missed(self):
+               with qui_utils.notify_error(self._errorLog):
+                       self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force=True)
 
-       @QtCore.pyqtSlot(str)
+       @qt_compat.Slot(str)
        @misc_utils.log_exception(_moduleLogger)
        def _on_session_error(self, message):
                with qui_utils.notify_error(self._errorLog):
                        self._errorLog.push_error(message)
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_login(self):
                with qui_utils.notify_error(self._errorLog):
@@ -597,15 +655,35 @@ class MainWindow(qwrappers.WindowWrapper):
                        for tab in self._tabsContents:
                                tab.enable()
                        self._initialize_tab(self._currentTab)
-
-       @QtCore.pyqtSlot()
+                       if self._updateVoicemailOnMissedCall:
+                               if self._callHandler is None:
+                                       import call_handler
+                                       self._callHandler = call_handler.MissedCallWatcher()
+                                       self._callHandler.callMissed.connect(self._voicemailRefreshDelay.start)
+                               self._callHandler.start()
+
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_logout(self):
                with qui_utils.notify_error(self._errorLog):
                        for tab in self._tabsContents:
                                tab.disable()
+                       if self._callHandler is not None:
+                               self._callHandler.stop()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_app_alert(self):
+               with qui_utils.notify_error(self._errorLog):
+                       if self._session.state == self._session.LOGGEDIN_STATE:
+                               messageType = {
+                                       (True, True): self._session.MESSAGE_ALL,
+                                       (True, False): self._session.MESSAGE_TEXTS,
+                                       (False, True): self._session.MESSAGE_VOICEMAILS,
+                               }[(self._app.notifyOnSms, self._app.notifyOnVoicemail)]
+                               self._session.update_messages(messageType, force=True)
+
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_recipients_changed(self):
                with qui_utils.notify_error(self._errorLog):
@@ -623,29 +701,38 @@ class MainWindow(qwrappers.WindowWrapper):
        def _on_child_close(self, obj = None):
                self._smsEntryDialog = None
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
        def _on_login_requested(self, checked = True):
                with qui_utils.notify_error(self._errorLog):
                        self._prompt_for_login()
 
-       @QtCore.pyqtSlot(int)
+       @qt_compat.Slot(int)
        @misc_utils.log_exception(_moduleLogger)
        def _on_tab_changed(self, index):
                with qui_utils.notify_error(self._errorLog):
                        self._currentTab = index
                        self._initialize_tab(index)
+                       if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
+                               self._app.ledHandler.off()
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
        def _on_refresh(self, checked = True):
                with qui_utils.notify_error(self._errorLog):
                        self._tabsContents[self._currentTab].refresh(force=True)
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_refresh_connection(self, checked = True):
+               with qui_utils.notify_error(self._errorLog):
+                       self._session.refresh_connection()
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
        def _on_import(self, checked = True):
                with qui_utils.notify_error(self._errorLog):
@@ -658,8 +745,8 @@ class MainWindow(qwrappers.WindowWrapper):
                        if self._tabsContents[self.CONTACTS_TAB].has_child:
                                self._tabsContents[self.CONTACTS_TAB].child.update_addressbooks()
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
        def _on_account(self, checked = True):
                with qui_utils.notify_error(self._errorLog):
@@ -668,6 +755,44 @@ class MainWindow(qwrappers.WindowWrapper):
 
 
 def run():
+       try:
+               os.makedirs(constants._data_path_)
+       except OSError, e:
+               if e.errno != 17:
+                       raise
+
+       logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+       logging.basicConfig(level=logging.DEBUG, format=logFormat)
+       rotating = logging.handlers.RotatingFileHandler(constants._user_logpath_, maxBytes=512*1024, backupCount=1)
+       rotating.setFormatter(logging.Formatter(logFormat))
+       root = logging.getLogger()
+       root.addHandler(rotating)
+       _moduleLogger.info("%s %s-%s" % (constants.__app_name__, constants.__version__, constants.__build__))
+       _moduleLogger.info("OS: %s" % (os.uname()[0], ))
+       _moduleLogger.info("Kernel: %s (%s) for %s" % os.uname()[2:])
+       _moduleLogger.info("Hostname: %s" % os.uname()[1])
+
+       try:
+               import gobject
+               gobject.threads_init()
+       except ImportError:
+               _moduleLogger.info("GObject support not available")
+       try:
+               import dbus
+               try:
+                       from dbus.mainloop.qt import DBusQtMainLoop
+                       DBusQtMainLoop(set_as_default=True)
+                       _moduleLogger.info("Using Qt mainloop")
+               except ImportError:
+                       try:
+                               from dbus.mainloop.glib import DBusGMainLoop
+                               DBusGMainLoop(set_as_default=True)
+                               _moduleLogger.info("Using GObject mainloop")
+                       except ImportError:
+                               _moduleLogger.info("Mainloop not available")
+       except ImportError:
+               _moduleLogger.info("DBus support not available")
+
        app = QtGui.QApplication([])
        handle = Dialcentral(app)
        qtpie.init_pies()
@@ -677,13 +802,5 @@ def run():
 if __name__ == "__main__":
        import sys
 
-       logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
-       logging.basicConfig(level=logging.DEBUG, format=logFormat)
-       try:
-               os.makedirs(constants._data_path_)
-       except OSError, e:
-               if e.errno != 17:
-                       raise
-
        val = run()
        sys.exit(val)
index 18d97e6..3258931 100644 (file)
@@ -7,8 +7,9 @@ import functools
 import copy
 import logging
 
-from PyQt4 import QtGui
-from PyQt4 import QtCore
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
 
 import constants
 from util import qwrappers
@@ -80,8 +81,8 @@ class CredentialsDialog(object):
                except RuntimeError:
                        _moduleLogger.exception("Oh well")
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
        def _on_close_window(self, checked = True):
                with qui_utils.notify_error(self._app.errorLog):
@@ -141,15 +142,15 @@ class AboutDialog(object):
                except RuntimeError:
                        _moduleLogger.exception("Oh well")
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
        def _on_close_window(self, checked = True):
                with qui_utils.notify_error(self._app.errorLog):
                        self._dialog.reject()
 
 
-class AccountDialog(object):
+class AccountDialog(QtCore.QObject, qwrappers.WindowWrapper):
 
        # @bug Can't enter custom callback numbers
 
@@ -169,13 +170,25 @@ class AccountDialog(object):
                (12*60, "12 hours"),
        ]
 
-       def __init__(self, app):
+       ALARM_NONE = "No Alert"
+       ALARM_BACKGROUND = "Background Alert"
+       ALARM_APPLICATION = "Application Alert"
+
+       VOICEMAIL_CHECK_NOT_SUPPORTED = "Not Supported"
+       VOICEMAIL_CHECK_DISABLED = "Disabled"
+       VOICEMAIL_CHECK_ENABLED = "Enabled"
+
+       settingsApproved = qt_compat.Signal()
+
+       def __init__(self, parent, app, errorLog):
+               QtCore.QObject.__init__(self)
+               qwrappers.WindowWrapper.__init__(self, parent, app)
                self._app = app
                self._doClear = False
 
                self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
-               self._notificationButton = QtGui.QCheckBox("Notifications")
-               self._notificationButton.stateChanged.connect(self._on_notification_change)
+               self._notificationSelecter = QtGui.QComboBox()
+               self._notificationSelecter.currentIndexChanged.connect(self._on_notification_change)
                self._notificationTimeSelector = QtGui.QComboBox()
                #self._notificationTimeSelector.setEditable(True)
                self._notificationTimeSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
@@ -184,11 +197,20 @@ class AccountDialog(object):
                self._missedCallsNotificationButton = QtGui.QCheckBox("Missed Calls")
                self._voicemailNotificationButton = QtGui.QCheckBox("Voicemail")
                self._smsNotificationButton = QtGui.QCheckBox("SMS")
+               self._voicemailOnMissedButton = QtGui.QCheckBox("Voicemail Update on Missed Calls")
                self._clearButton = QtGui.QPushButton("Clear Account")
                self._clearButton.clicked.connect(self._on_clear)
                self._callbackSelector = QtGui.QComboBox()
                #self._callbackSelector.setEditable(True)
                self._callbackSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
+               self._orientationSelector = QtGui.QComboBox()
+               for orientationMode in [
+                       self._app.DEFAULT_ORIENTATION,
+                       self._app.AUTO_ORIENTATION,
+                       self._app.LANDSCAPE_ORIENTATION,
+                       self._app.PORTRAIT_ORIENTATION,
+               ]:
+                       self._orientationSelector.addItem(orientationMode)
 
                self._update_notification_state()
 
@@ -197,7 +219,7 @@ class AccountDialog(object):
                self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
                self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
                self._credLayout.addWidget(self._callbackSelector, 1, 1)
-               self._credLayout.addWidget(self._notificationButton, 2, 0)
+               self._credLayout.addWidget(self._notificationSelecter, 2, 0)
                self._credLayout.addWidget(self._notificationTimeSelector, 2, 1)
                self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
                self._credLayout.addWidget(self._missedCallsNotificationButton, 3, 1)
@@ -205,33 +227,37 @@ class AccountDialog(object):
                self._credLayout.addWidget(self._voicemailNotificationButton, 4, 1)
                self._credLayout.addWidget(QtGui.QLabel(""), 5, 0)
                self._credLayout.addWidget(self._smsNotificationButton, 5, 1)
-
-               self._credLayout.addWidget(QtGui.QLabel(""), 6, 0)
-               self._credLayout.addWidget(self._clearButton, 6, 1)
-               self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
-
-               self._loginButton = QtGui.QPushButton("&Apply")
-               self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
-               self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
-
-               self._layout = QtGui.QVBoxLayout()
-               self._layout.addLayout(self._credLayout)
+               self._credLayout.addWidget(QtGui.QLabel("Other"), 6, 0)
+               self._credLayout.addWidget(self._voicemailOnMissedButton, 6, 1)
+               self._credLayout.addWidget(QtGui.QLabel("Orientation"), 7, 0)
+               self._credLayout.addWidget(self._orientationSelector, 7, 1)
+               self._credLayout.addWidget(QtGui.QLabel(""), 8, 0)
+               self._credLayout.addWidget(QtGui.QLabel(""), 9, 0)
+               self._credLayout.addWidget(self._clearButton, 9, 1)
+
+               self._credWidget = QtGui.QWidget()
+               self._credWidget.setLayout(self._credLayout)
+               self._credWidget.setContentsMargins(0, 0, 0, 0)
+               self._scrollSettings = QtGui.QScrollArea()
+               self._scrollSettings.setWidget(self._credWidget)
+               self._scrollSettings.setWidgetResizable(True)
+               self._scrollSettings.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+               self._scrollSettings.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+               self._applyButton = QtGui.QPushButton("&Apply")
+               self._applyButton.clicked.connect(self._on_settings_apply)
+               self._cancelButton = QtGui.QPushButton("&Cancel")
+               self._cancelButton.clicked.connect(self._on_settings_cancel)
+               self._buttonLayout = QtGui.QDialogButtonBox()
+               self._buttonLayout.addButton(self._applyButton, QtGui.QDialogButtonBox.AcceptRole)
+               self._buttonLayout.addButton(self._cancelButton, QtGui.QDialogButtonBox.RejectRole)
+
+               self._layout.addWidget(self._scrollSettings)
                self._layout.addWidget(self._buttonLayout)
+               self._layout.setDirection(QtGui.QBoxLayout.TopToBottom)
 
-               self._dialog = QtGui.QDialog()
-               self._dialog.setWindowTitle("Account")
-               self._dialog.setLayout(self._layout)
-               self._buttonLayout.accepted.connect(self._dialog.accept)
-               self._buttonLayout.rejected.connect(self._dialog.reject)
-
-               self._closeWindowAction = QtGui.QAction(None)
-               self._closeWindowAction.setText("Close")
-               self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
-               self._closeWindowAction.triggered.connect(self._on_close_window)
-
-               self._dialog.addAction(self._closeWindowAction)
-               self._dialog.addAction(app.quitAction)
-               self._dialog.addAction(app.fullscreenAction)
+               self._window.setWindowTitle("Account")
+               self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
 
        @property
        def doClear(self):
@@ -239,24 +265,54 @@ class AccountDialog(object):
 
        def setIfNotificationsSupported(self, isSupported):
                if isSupported:
-                       self._notificationButton.setVisible(True)
-                       self._notificationTimeSelector.setVisible(True)
-                       self._missedCallsNotificationButton.setVisible(True)
-                       self._voicemailNotificationButton.setVisible(True)
-                       self._smsNotificationButton.setVisible(True)
+                       self._notificationSelecter.clear()
+                       self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION, self.ALARM_BACKGROUND])
+                       self._notificationTimeSelector.setEnabled(False)
+                       self._missedCallsNotificationButton.setEnabled(False)
+                       self._voicemailNotificationButton.setEnabled(False)
+                       self._smsNotificationButton.setEnabled(False)
                else:
-                       self._notificationButton.setVisible(False)
-                       self._notificationTimeSelector.setVisible(False)
-                       self._missedCallsNotificationButton.setVisible(False)
-                       self._voicemailNotificationButton.setVisible(False)
-                       self._smsNotificationButton.setVisible(False)
+                       self._notificationSelecter.clear()
+                       self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION])
+                       self._notificationTimeSelector.setEnabled(False)
+                       self._missedCallsNotificationButton.setEnabled(False)
+                       self._voicemailNotificationButton.setEnabled(False)
+                       self._smsNotificationButton.setEnabled(False)
 
        def set_account_number(self, num):
                self._accountNumberLabel.setText(num)
 
+       orientation = property(
+               lambda self: str(self._orientationSelector.currentText()),
+               lambda self, mode: qui_utils.set_current_index(self._orientationSelector, mode),
+       )
+
+       def _set_voicemail_on_missed(self, status):
+               if status == self.VOICEMAIL_CHECK_NOT_SUPPORTED:
+                       self._voicemailOnMissedButton.setChecked(False)
+                       self._voicemailOnMissedButton.hide()
+               elif status == self.VOICEMAIL_CHECK_DISABLED:
+                       self._voicemailOnMissedButton.setChecked(False)
+                       self._voicemailOnMissedButton.show()
+               elif status == self.VOICEMAIL_CHECK_ENABLED:
+                       self._voicemailOnMissedButton.setChecked(True)
+                       self._voicemailOnMissedButton.show()
+               else:
+                       raise RuntimeError("Unsupported option for updating voicemail on missed calls %r" % status)
+
+       def _get_voicemail_on_missed(self):
+               if not self._voicemailOnMissedButton.isVisible():
+                       return self.VOICEMAIL_CHECK_NOT_SUPPORTED
+               elif self._voicemailOnMissedButton.isChecked():
+                       return self.VOICEMAIL_CHECK_ENABLED
+               else:
+                       return self.VOICEMAIL_CHECK_DISABLED
+
+       updateVMOnMissedCall = property(_get_voicemail_on_missed, _set_voicemail_on_missed)
+
        notifications = property(
-               lambda self: self._notificationButton.isChecked(),
-               lambda self, enabled: self._notificationButton.setChecked(enabled),
+               lambda self: str(self._notificationSelecter.currentText()),
+               lambda self, enabled: qui_utils.set_current_index(self._notificationSelecter, enabled),
        )
 
        notifyOnMissed = property(
@@ -292,7 +348,7 @@ class AccountDialog(object):
        @property
        def selectedCallback(self):
                index = self._callbackSelector.currentIndex()
-               data = str(self._callbackSelector.itemData(index).toPyObject())
+               data = str(self._callbackSelector.itemData(index))
                return data
 
        def set_callbacks(self, choices, default):
@@ -311,51 +367,72 @@ class AccountDialog(object):
                        if uglyNumber == uglyDefault:
                                self._callbackSelector.setCurrentIndex(self._callbackSelector.count() - 1)
 
-       def run(self, parent=None):
+       def run(self):
                self._doClear = False
-               self._dialog.setParent(parent, QtCore.Qt.Dialog)
-
-               response = self._dialog.exec_()
-               return response
+               self._window.show()
 
        def close(self):
                try:
-                       self._dialog.reject()
+                       self._window.hide()
                except RuntimeError:
                        _moduleLogger.exception("Oh well")
 
        def _update_notification_state(self):
-               if self._notificationButton.isChecked():
+               currentText = str(self._notificationSelecter.currentText())
+               if currentText == self.ALARM_BACKGROUND:
                        self._notificationTimeSelector.setEnabled(True)
+
                        self._missedCallsNotificationButton.setEnabled(True)
                        self._voicemailNotificationButton.setEnabled(True)
                        self._smsNotificationButton.setEnabled(True)
+               elif currentText == self.ALARM_APPLICATION:
+                       self._notificationTimeSelector.setEnabled(True)
+
+                       self._missedCallsNotificationButton.setEnabled(False)
+                       self._voicemailNotificationButton.setEnabled(True)
+                       self._smsNotificationButton.setEnabled(True)
+
+                       self._missedCallsNotificationButton.setChecked(False)
                else:
                        self._notificationTimeSelector.setEnabled(False)
+
                        self._missedCallsNotificationButton.setEnabled(False)
                        self._voicemailNotificationButton.setEnabled(False)
                        self._smsNotificationButton.setEnabled(False)
 
-       @QtCore.pyqtSlot(int)
+                       self._missedCallsNotificationButton.setChecked(False)
+                       self._voicemailNotificationButton.setChecked(False)
+                       self._smsNotificationButton.setChecked(False)
+
+       @qt_compat.Slot(int)
        @misc_utils.log_exception(_moduleLogger)
-       def _on_notification_change(self, state):
+       def _on_notification_change(self, index):
                with qui_utils.notify_error(self._app.errorLog):
                        self._update_notification_state()
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
-       def _on_clear(self, checked = False):
+       def _on_settings_cancel(self, checked = False):
                with qui_utils.notify_error(self._app.errorLog):
-                       self._doClear = True
-                       self._dialog.accept()
+                       self.hide()
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
-       def _on_close_window(self, checked = True):
+       def _on_settings_apply(self, checked = False):
                with qui_utils.notify_error(self._app.errorLog):
-                       self._dialog.reject()
+                       self.settingsApproved.emit()
+                       self.hide()
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_clear(self, checked = False):
+               with qui_utils.notify_error(self._app.errorLog):
+                       self._doClear = True
+                       self.settingsApproved.emit()
+                       self.hide()
 
 
 class ContactList(object):
@@ -415,7 +492,7 @@ class ContactList(object):
                        )
                        callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
                        numberSelector.activated.connect(
-                               QtCore.pyqtSlot(int)(callback)
+                               qt_compat.Slot(int)(callback)
                        )
 
                        if self._closeIcon is self._SENTINEL_ICON:
@@ -504,6 +581,216 @@ class ContactList(object):
                        self._session.draft.remove_contact(self._uiItems[index]["cid"])
 
 
+class VoicemailPlayer(object):
+
+       def __init__(self, app, session, errorLog):
+               self._app = app
+               self._session = session
+               self._errorLog = errorLog
+               self._token = None
+               self._session.voicemailAvailable.connect(self._on_voicemail_downloaded)
+               self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
+
+               self._playButton = QtGui.QPushButton("Play")
+               self._playButton.clicked.connect(self._on_voicemail_play)
+               self._pauseButton = QtGui.QPushButton("Pause")
+               self._pauseButton.clicked.connect(self._on_voicemail_pause)
+               self._pauseButton.hide()
+               self._resumeButton = QtGui.QPushButton("Resume")
+               self._resumeButton.clicked.connect(self._on_voicemail_resume)
+               self._resumeButton.hide()
+               self._stopButton = QtGui.QPushButton("Stop")
+               self._stopButton.clicked.connect(self._on_voicemail_stop)
+               self._stopButton.hide()
+
+               self._downloadButton = QtGui.QPushButton("Download Voicemail")
+               self._downloadButton.clicked.connect(self._on_voicemail_download)
+               self._downloadLayout = QtGui.QHBoxLayout()
+               self._downloadLayout.addWidget(self._downloadButton)
+               self._downloadWidget = QtGui.QWidget()
+               self._downloadWidget.setLayout(self._downloadLayout)
+
+               self._playLabel = QtGui.QLabel("Voicemail")
+               self._saveButton = QtGui.QPushButton("Save")
+               self._saveButton.clicked.connect(self._on_voicemail_save)
+               self._playerLayout = QtGui.QHBoxLayout()
+               self._playerLayout.addWidget(self._playLabel)
+               self._playerLayout.addWidget(self._playButton)
+               self._playerLayout.addWidget(self._pauseButton)
+               self._playerLayout.addWidget(self._resumeButton)
+               self._playerLayout.addWidget(self._stopButton)
+               self._playerLayout.addWidget(self._saveButton)
+               self._playerWidget = QtGui.QWidget()
+               self._playerWidget.setLayout(self._playerLayout)
+
+               self._visibleWidget = None
+               self._layout = QtGui.QHBoxLayout()
+               self._layout.setContentsMargins(0, 0, 0, 0)
+               self._widget = QtGui.QWidget()
+               self._widget.setLayout(self._layout)
+               self._update_state()
+
+       @property
+       def toplevel(self):
+               return self._widget
+
+       def destroy(self):
+               self._session.voicemailAvailable.disconnect(self._on_voicemail_downloaded)
+               self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
+               self._invalidate_token()
+
+       def _invalidate_token(self):
+               if self._token is not None:
+                       self._token.invalidate()
+                       self._token.error.disconnect(self._on_play_error)
+                       self._token.stateChange.connect(self._on_play_state)
+                       self._token.invalidated.connect(self._on_play_invalidated)
+
+       def _show_download(self, messageId):
+               if self._visibleWidget is self._downloadWidget:
+                       return
+               self._hide()
+               self._layout.addWidget(self._downloadWidget)
+               self._visibleWidget = self._downloadWidget
+               self._visibleWidget.show()
+
+       def _show_player(self, messageId):
+               if self._visibleWidget is self._playerWidget:
+                       return
+               self._hide()
+               self._layout.addWidget(self._playerWidget)
+               self._visibleWidget = self._playerWidget
+               self._visibleWidget.show()
+
+       def _hide(self):
+               if self._visibleWidget is None:
+                       return
+               self._visibleWidget.hide()
+               self._layout.removeWidget(self._visibleWidget)
+               self._visibleWidget = None
+
+       def _update_play_state(self):
+               if self._token is not None and self._token.isValid:
+                       self._playButton.setText("Stop")
+               else:
+                       self._playButton.setText("Play")
+
+       def _update_state(self):
+               if self._session.draft.get_num_contacts() != 1:
+                       self._hide()
+                       return
+
+               (cid, ) = self._session.draft.get_contacts()
+               messageId = self._session.draft.get_message_id(cid)
+               if messageId is None:
+                       self._hide()
+                       return
+
+               if self._session.is_available(messageId):
+                       self._show_player(messageId)
+               else:
+                       self._show_download(messageId)
+               if self._token is not None:
+                       self._token.invalidate()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_voicemail_save(self, arg):
+               with qui_utils.notify_error(self._app.errorLog):
+                       targetPath = QtGui.QFileDialog.getSaveFileName(None, caption="Save Voicemail", filter="Audio File (*.mp3)")
+                       targetPath = unicode(targetPath)
+                       if not targetPath:
+                               return
+
+                       (cid, ) = self._session.draft.get_contacts()
+                       messageId = self._session.draft.get_message_id(cid)
+                       sourcePath = self._session.voicemail_path(messageId)
+                       import shutil
+                       shutil.copy2(sourcePath, targetPath)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_play_error(self, error):
+               with qui_utils.notify_error(self._app.errorLog):
+                       self._app.errorLog.push_error(error)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_play_invalidated(self):
+               with qui_utils.notify_error(self._app.errorLog):
+                       self._playButton.show()
+                       self._pauseButton.hide()
+                       self._resumeButton.hide()
+                       self._stopButton.hide()
+                       self._invalidate_token()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_play_state(self, state):
+               with qui_utils.notify_error(self._app.errorLog):
+                       if state == self._token.STATE_PLAY:
+                               self._playButton.hide()
+                               self._pauseButton.show()
+                               self._resumeButton.hide()
+                               self._stopButton.show()
+                       elif state == self._token.STATE_PAUSE:
+                               self._playButton.hide()
+                               self._pauseButton.hide()
+                               self._resumeButton.show()
+                               self._stopButton.show()
+                       elif state == self._token.STATE_STOP:
+                               self._playButton.show()
+                               self._pauseButton.hide()
+                               self._resumeButton.hide()
+                               self._stopButton.hide()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_voicemail_play(self, arg):
+               with qui_utils.notify_error(self._app.errorLog):
+                       (cid, ) = self._session.draft.get_contacts()
+                       messageId = self._session.draft.get_message_id(cid)
+                       sourcePath = self._session.voicemail_path(messageId)
+
+                       self._invalidate_token()
+                       uri = "file://%s" % sourcePath
+                       self._token = self._app.streamHandler.set_file(uri)
+                       self._token.stateChange.connect(self._on_play_state)
+                       self._token.invalidated.connect(self._on_play_invalidated)
+                       self._token.error.connect(self._on_play_error)
+                       self._token.play()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_voicemail_pause(self, arg):
+               with qui_utils.notify_error(self._app.errorLog):
+                       self._token.pause()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_voicemail_resume(self, arg):
+               with qui_utils.notify_error(self._app.errorLog):
+                       self._token.play()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_voicemail_stop(self, arg):
+               with qui_utils.notify_error(self._app.errorLog):
+                       self._token.stop()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_voicemail_download(self, arg):
+               with qui_utils.notify_error(self._app.errorLog):
+                       (cid, ) = self._session.draft.get_contacts()
+                       messageId = self._session.draft.get_message_id(cid)
+                       self._session.download_voicemail(messageId)
+                       self._hide()
+
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_recipients_changed(self):
+               with qui_utils.notify_error(self._app.errorLog):
+                       self._update_state()
+
+       @qt_compat.Slot(str, str)
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_voicemail_downloaded(self, messageId, filepath):
+               with qui_utils.notify_error(self._app.errorLog):
+                       self._update_state()
+
+
 class SMSEntryWindow(qwrappers.WindowWrapper):
 
        MAX_CHAR = 160
@@ -525,20 +812,21 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
                self._session.draft.called.connect(self._on_op_finished)
                self._session.draft.cancelled.connect(self._on_op_finished)
                self._session.draft.error.connect(self._on_op_error)
-               self._errorLog = errorLog
 
-               self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
+               self._errorLog = errorLog
 
                self._targetList = ContactList(self._app, self._session)
                self._history = QtGui.QLabel()
                self._history.setTextFormat(QtCore.Qt.RichText)
                self._history.setWordWrap(True)
+               self._voicemailPlayer = VoicemailPlayer(self._app, self._session, self._errorLog)
                self._smsEntry = QtGui.QTextEdit()
                self._smsEntry.textChanged.connect(self._on_letter_count_changed)
 
                self._entryLayout = QtGui.QVBoxLayout()
                self._entryLayout.addWidget(self._targetList.toplevel)
                self._entryLayout.addWidget(self._history)
+               self._entryLayout.addWidget(self._voicemailPlayer.toplevel, 0)
                self._entryLayout.addWidget(self._smsEntry)
                self._entryLayout.setContentsMargins(0, 0, 0, 0)
                self._entryWidget = QtGui.QWidget()
@@ -579,6 +867,7 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
                self._window.setWindowTitle("Contact")
                self._window.closed.connect(self._on_close_window)
                self._window.hidden.connect(self._on_close_window)
+               self._window.resized.connect(self._on_window_resized)
 
                self._scrollTimer = QtCore.QTimer()
                self._scrollTimer.setInterval(100)
@@ -589,7 +878,7 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
                self._update_letter_count()
                self._update_target_fields()
                self.set_fullscreen(self._app.fullscreenAction.isChecked())
-               self.set_orientation(self._app.orientationAction.isChecked())
+               self.update_orientation(self._app.orientation)
 
        def close(self):
                if self._window is None:
@@ -617,6 +906,7 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
                self._session.draft.called.disconnect(self._on_op_finished)
                self._session.draft.cancelled.disconnect(self._on_op_finished)
                self._session.draft.error.disconnect(self._on_op_error)
+               self._voicemailPlayer.destroy()
                window = self._window
                self._window = None
                try:
@@ -627,12 +917,12 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
                except RuntimeError:
                        _moduleLogger.exception("Oh well")
 
-       def set_orientation(self, isPortrait):
-               qwrappers.WindowWrapper.set_orientation(self, isPortrait)
+       def update_orientation(self, orientation):
+               qwrappers.WindowWrapper.update_orientation(self, orientation)
                self._scroll_to_bottom()
 
        def _update_letter_count(self):
-               count = self._smsEntry.toPlainText().size()
+               count = len(self._smsEntry.toPlainText())
                numTexts, numCharInText = divmod(count, self.MAX_CHAR)
                numTexts += 1
                numCharsLeftInText = self.MAX_CHAR - numCharInText
@@ -644,7 +934,7 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
                        self._dialButton.setEnabled(False)
                        self._smsButton.setEnabled(False)
                elif self._session.draft.get_num_contacts() == 1:
-                       count = self._smsEntry.toPlainText().size()
+                       count = len(self._smsEntry.toPlainText())
                        if count == 0:
                                self._dialButton.setEnabled(True)
                                self._smsButton.setEnabled(False)
@@ -653,7 +943,7 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
                                self._smsButton.setEnabled(True)
                else:
                        self._dialButton.setEnabled(False)
-                       count = self._smsEntry.toPlainText().size()
+                       count = len(self._smsEntry.toPlainText())
                        if count == 0:
                                self._smsButton.setEnabled(False)
                        else:
@@ -752,7 +1042,7 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
                        self._session.draft.message = message
                        self._session.draft.call()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_cancel_clicked(self, message):
                with qui_utils.notify_error(self._app.errorLog):
@@ -771,24 +1061,25 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
                        number = numbers[index][0]
                        self._session.draft.set_selected_number(cid, number)
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_refresh_history(self):
-               draftContactsCount = self._session.draft.get_num_contacts()
-               if draftContactsCount != 1:
-                       # Changing contact count will automatically refresh it
-                       return
-               (cid, ) = self._session.draft.get_contacts()
-               self._update_history(cid)
+               with qui_utils.notify_error(self._app.errorLog):
+                       draftContactsCount = self._session.draft.get_num_contacts()
+                       if draftContactsCount != 1:
+                               # Changing contact count will automatically refresh it
+                               return
+                       (cid, ) = self._session.draft.get_contacts()
+                       self._update_history(cid)
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_recipients_changed(self):
                with qui_utils.notify_error(self._app.errorLog):
                        self._update_target_fields()
                        self._update_button_state()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_op_started(self):
                with qui_utils.notify_error(self._app.errorLog):
@@ -797,13 +1088,13 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
                        self._dialButton.setVisible(False)
                        self.show()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_calling_started(self):
                with qui_utils.notify_error(self._app.errorLog):
                        self._cancelButton.setVisible(True)
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_op_finished(self):
                with qui_utils.notify_error(self._app.errorLog):
@@ -815,7 +1106,7 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
                        self.close()
                        self.destroy()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_op_error(self, message):
                with qui_utils.notify_error(self._app.errorLog):
@@ -826,15 +1117,21 @@ class SMSEntryWindow(qwrappers.WindowWrapper):
 
                        self._errorLog.push_error(message)
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_letter_count_changed(self):
                with qui_utils.notify_error(self._app.errorLog):
                        self._update_letter_count()
                        self._update_button_state()
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_window_resized(self):
+               with qui_utils.notify_error(self._app.errorLog):
+                       self._scroll_to_bottom()
+
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
        def _on_close_window(self, checked = True):
                with qui_utils.notify_error(self._app.errorLog):
index 2318549..541ac18 100644 (file)
@@ -15,7 +15,6 @@ import alarm_notify
 
 
 def notify_on_change():
-       filename = "%s/notification.log" % constants._data_path_
        with open(constants._notifier_logpath_, "a") as file:
                file.write("Notification: %r\n" % (datetime.datetime.now(), ))
 
index 6aba9c3..db05d11 100644 (file)
@@ -8,8 +8,9 @@ import string
 import itertools
 import logging
 
-from PyQt4 import QtGui
-from PyQt4 import QtCore
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
 
 from util import qtpie
 from util import qui_utils
@@ -17,11 +18,15 @@ from util import misc as misc_utils
 
 import backends.null_backend as null_backend
 import backends.file_backend as file_backend
+import backends.qt_backend as qt_backend
 
 
 _moduleLogger = logging.getLogger(__name__)
 
 
+_SENTINEL_ICON = QtGui.QIcon()
+
+
 class Dialpad(object):
 
        def __init__(self, app, session, errorLog):
@@ -156,8 +161,8 @@ class Dialpad(object):
                with qui_utils.notify_error(self._errorLog):
                        self._entry.clear()
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
        def _on_sms_clicked(self, checked = False):
                with qui_utils.notify_error(self._errorLog):
@@ -168,10 +173,10 @@ class Dialpad(object):
                        title = misc_utils.make_pretty(number)
                        description = misc_utils.make_pretty(number)
                        numbersWithDescriptions = [(number, "")]
-                       self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
+                       self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc_utils.log_exception(_moduleLogger)
        def _on_call_clicked(self, checked = False):
                with qui_utils.notify_error(self._errorLog):
@@ -183,7 +188,7 @@ class Dialpad(object):
                        description = misc_utils.make_pretty(number)
                        numbersWithDescriptions = [(number, "")]
                        self._session.draft.clear()
-                       self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
+                       self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
                        self._session.draft.call()
 
 
@@ -260,7 +265,12 @@ class History(object):
        FROM_IDX = 1
        MAX_IDX = 2
 
-       HISTORY_ITEM_TYPES = ["Received", "Missed", "Placed", "All"]
+       HISTORY_RECEIVED = "Received"
+       HISTORY_MISSED = "Missed"
+       HISTORY_PLACED = "Placed"
+       HISTORY_ALL = "All"
+
+       HISTORY_ITEM_TYPES = [HISTORY_RECEIVED, HISTORY_MISSED, HISTORY_PLACED, HISTORY_ALL]
        HISTORY_COLUMNS = ["Details", "From"]
        assert len(HISTORY_COLUMNS) == MAX_IDX
 
@@ -278,9 +288,13 @@ class History(object):
                )
                self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
                refreshIcon = qui_utils.get_theme_icon(
-                       ("view-refresh", "general_refresh", "gtk-refresh", )
+                       ("view-refresh", "general_refresh", "gtk-refresh", ),
+                       _SENTINEL_ICON
                )
-               self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+               if refreshIcon is not _SENTINEL_ICON:
+                       self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+               else:
+                       self._refreshButton = QtGui.QPushButton("Refresh")
                self._refreshButton.clicked.connect(self._on_refresh_clicked)
                self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
                        QtGui.QSizePolicy.Minimum,
@@ -343,8 +357,19 @@ class History(object):
 
        def refresh(self, force=True):
                self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
-               self._session.update_history(force)
-               if self._app.notifyOnMissed:
+
+               if self._selectedFilter == self.HISTORY_RECEIVED:
+                       self._session.update_history(self._session.HISTORY_RECEIVED, force)
+               elif self._selectedFilter == self.HISTORY_MISSED:
+                       self._session.update_history(self._session.HISTORY_MISSED, force)
+               elif self._selectedFilter == self.HISTORY_PLACED:
+                       self._session.update_history(self._session.HISTORY_PLACED, force)
+               elif self._selectedFilter == self.HISTORY_ALL:
+                       self._session.update_history(self._session.HISTORY_ALL, force)
+               else:
+                       assert False, "How did we get here?"
+
+               if self._app.notifyOnMissed and self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_BACKGROUND:
                        self._app.ledHandler.off()
 
        def _populate_items(self):
@@ -382,26 +407,26 @@ class History(object):
                        self._categoryManager.add_row(event["time"], row)
                self._itemView.expandAll()
 
-       @QtCore.pyqtSlot(str)
+       @qt_compat.Slot(str)
        @misc_utils.log_exception(_moduleLogger)
        def _on_filter_changed(self, newItem):
                with qui_utils.notify_error(self._errorLog):
                        self._selectedFilter = str(newItem)
                        self._populate_items()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_history_updated(self):
                with qui_utils.notify_error(self._errorLog):
                        self._populate_items()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_refresh_clicked(self, arg = None):
                with qui_utils.notify_error(self._errorLog):
                        self.refresh(force=True)
 
-       @QtCore.pyqtSlot(QtCore.QModelIndex)
+       @qt_compat.Slot(QtCore.QModelIndex)
        @misc_utils.log_exception(_moduleLogger)
        def _on_row_activated(self, index):
                with qui_utils.notify_error(self._errorLog):
@@ -412,10 +437,10 @@ class History(object):
                        row = index.row()
                        detailsItem = self._categoryManager.get_item(timeRow, row, self.DETAILS_IDX)
                        fromItem = self._categoryManager.get_item(timeRow, row, self.FROM_IDX)
-                       contactDetails = detailsItem.data().toPyObject()
+                       contactDetails = detailsItem.data()
 
                        title = unicode(fromItem.text())
-                       number = str(contactDetails[QtCore.QString("number")])
+                       number = str(contactDetails["number"])
                        contactId = number # ids don't seem too unique so using numbers
 
                        descriptionRows = []
@@ -423,19 +448,19 @@ class History(object):
                                randomTimeItem = self._itemStore.item(t, 0)
                                for i in xrange(randomTimeItem.rowCount()):
                                        iItem = randomTimeItem.child(i, 0)
-                                       iContactDetails = iItem.data().toPyObject()
-                                       iNumber = str(iContactDetails[QtCore.QString("number")])
+                                       iContactDetails = iItem.data()
+                                       iNumber = str(iContactDetails["number"])
                                        if number != iNumber:
                                                continue
-                                       relTime = misc_utils.abbrev_relative_date(iContactDetails[QtCore.QString("relTime")])
-                                       action = str(iContactDetails[QtCore.QString("action")])
-                                       number = str(iContactDetails[QtCore.QString("number")])
+                                       relTime = misc_utils.abbrev_relative_date(iContactDetails["relTime"])
+                                       action = str(iContactDetails["action"])
+                                       number = str(iContactDetails["number"])
                                        prettyNumber = misc_utils.make_pretty(number)
                                        rowItems = relTime, action, prettyNumber
                                        descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
                        description = "<table>%s</table>" % "".join(descriptionRows)
-                       numbersWithDescriptions = [(str(contactDetails[QtCore.QString("number")]), "")]
-                       self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
+                       numbersWithDescriptions = [(str(contactDetails["number"]), "")]
+                       self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
 
 
 class Messages(object):
@@ -476,9 +501,13 @@ class Messages(object):
                self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
 
                refreshIcon = qui_utils.get_theme_icon(
-                       ("view-refresh", "general_refresh", "gtk-refresh", )
+                       ("view-refresh", "general_refresh", "gtk-refresh", ),
+                       _SENTINEL_ICON
                )
-               self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+               if refreshIcon is not _SENTINEL_ICON:
+                       self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+               else:
+                       self._refreshButton = QtGui.QPushButton("Refresh")
                self._refreshButton.clicked.connect(self._on_refresh_clicked)
                self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
                        QtGui.QSizePolicy.Minimum,
@@ -553,8 +582,19 @@ class Messages(object):
 
        def refresh(self, force=True):
                self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
-               self._session.update_messages(force)
-               if self._app.notifyOnSms or self._app.notifyOnVoicemail:
+
+               if self._selectedTypeFilter == self.NO_MESSAGES:
+                       pass
+               elif self._selectedTypeFilter == self.TEXT_MESSAGES:
+                       self._session.update_messages(self._session.MESSAGE_TEXTS, force)
+               elif self._selectedTypeFilter == self.VOICEMAIL_MESSAGES:
+                       self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force)
+               elif self._selectedTypeFilter == self.ALL_TYPES:
+                       self._session.update_messages(self._session.MESSAGE_ALL, force)
+               else:
+                       assert False, "How did we get here?"
+
+               if self._app.notifyOnSms or self._app.notifyOnVoicemail and self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_BACKGROUND:
                        self._app.ledHandler.off()
 
        def _populate_items(self):
@@ -620,33 +660,33 @@ class Messages(object):
                        self._categoryManager.add_row(item["time"], row)
                self._itemView.expandAll()
 
-       @QtCore.pyqtSlot(str)
+       @qt_compat.Slot(str)
        @misc_utils.log_exception(_moduleLogger)
        def _on_type_filter_changed(self, newItem):
                with qui_utils.notify_error(self._errorLog):
                        self._selectedTypeFilter = str(newItem)
                        self._populate_items()
 
-       @QtCore.pyqtSlot(str)
+       @qt_compat.Slot(str)
        @misc_utils.log_exception(_moduleLogger)
        def _on_status_filter_changed(self, newItem):
                with qui_utils.notify_error(self._errorLog):
                        self._selectedStatusFilter = str(newItem)
                        self._populate_items()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_refresh_clicked(self, arg = None):
                with qui_utils.notify_error(self._errorLog):
                        self.refresh(force=True)
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_messages_updated(self):
                with qui_utils.notify_error(self._errorLog):
                        self._populate_items()
 
-       @QtCore.pyqtSlot(QtCore.QModelIndex)
+       @qt_compat.Slot(QtCore.QModelIndex)
        @misc_utils.log_exception(_moduleLogger)
        def _on_row_activated(self, index):
                with qui_utils.notify_error(self._errorLog):
@@ -656,22 +696,26 @@ class Messages(object):
                        timeRow = timeIndex.row()
                        row = index.row()
                        item = self._categoryManager.get_item(timeRow, row, 0)
-                       contactDetails = item.data().toPyObject()
+                       contactDetails = item.data()
 
-                       name = unicode(contactDetails[QtCore.QString("name")])
-                       number = str(contactDetails[QtCore.QString("number")])
+                       name = unicode(contactDetails["name"])
+                       number = str(contactDetails["number"])
                        if not name or name == number:
-                               name = unicode(contactDetails[QtCore.QString("location")])
+                               name = unicode(contactDetails["location"])
                        if not name:
                                name = "Unknown"
 
-                       contactId = str(contactDetails[QtCore.QString("id")])
+                       if str(contactDetails["type"]) == "Voicemail":
+                               messageId = str(contactDetails["id"])
+                       else:
+                               messageId = None
+                       contactId = str(contactDetails["contactId"])
                        title = name
-                       description = unicode(contactDetails[QtCore.QString("expandedMessages")])
+                       description = unicode(contactDetails["expandedMessages"])
                        numbersWithDescriptions = [(number, "")]
-                       self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
+                       self._session.draft.add_contact(contactId, messageId, title, description, numbersWithDescriptions)
 
-       @QtCore.pyqtSlot(QtCore.QModelIndex)
+       @qt_compat.Slot(QtCore.QModelIndex)
        @misc_utils.log_exception(_moduleLogger)
        def _on_column_resized(self, index, oldSize, newSize):
                self._htmlDelegate.setWidth(newSize, self._itemStore)
@@ -684,11 +728,12 @@ class Contacts(object):
        def __init__(self, app, session, errorLog):
                self._app = app
                self._session = session
-               self._session.contactsUpdated.connect(self._on_contacts_updated)
+               self._session.accountUpdated.connect(self._on_contacts_updated)
                self._errorLog = errorLog
                self._addressBookFactories = [
                        null_backend.NullAddressBookFactory(),
                        file_backend.FilesystemAddressBookFactory(app.fsContactsPath),
+                       qt_backend.QtContactsAddressBookFactory(),
                ]
                self._addressBooks = []
 
@@ -697,9 +742,13 @@ class Contacts(object):
                self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
                self._activeList = "None"
                refreshIcon = qui_utils.get_theme_icon(
-                       ("view-refresh", "general_refresh", "gtk-refresh", )
+                       ("view-refresh", "general_refresh", "gtk-refresh", ),
+                       _SENTINEL_ICON
                )
-               self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+               if refreshIcon is not _SENTINEL_ICON:
+                       self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+               else:
+                       self._refreshButton = QtGui.QPushButton("Refresh")
                self._refreshButton.clicked.connect(self._on_refresh_clicked)
                self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
                        QtGui.QSizePolicy.Minimum,
@@ -765,7 +814,7 @@ class Contacts(object):
 
        def refresh(self, force=True):
                self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
-               self._backend.update_contacts(force)
+               self._backend.update_account(force)
 
        @property
        def _backend(self):
@@ -849,7 +898,7 @@ class Contacts(object):
                contacts.sort(key=lambda contact: contact["name"].lower())
                return contacts
 
-       @QtCore.pyqtSlot(str)
+       @qt_compat.Slot(str)
        @misc_utils.log_exception(_moduleLogger)
        def _on_filter_changed(self, newItem):
                with qui_utils.notify_error(self._errorLog):
@@ -857,19 +906,19 @@ class Contacts(object):
                        self.refresh(force=False)
                        self._populate_items()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_refresh_clicked(self, arg = None):
                with qui_utils.notify_error(self._errorLog):
                        self.refresh(force=True)
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc_utils.log_exception(_moduleLogger)
        def _on_contacts_updated(self):
                with qui_utils.notify_error(self._errorLog):
                        self._populate_items()
 
-       @QtCore.pyqtSlot(QtCore.QModelIndex)
+       @qt_compat.Slot(QtCore.QModelIndex)
        @misc_utils.log_exception(_moduleLogger)
        def _on_row_activated(self, index):
                with qui_utils.notify_error(self._errorLog):
@@ -881,16 +930,16 @@ class Contacts(object):
                        letterItem = self._alphaItem[letter]
                        rowIndex = index.row()
                        item = letterItem.child(rowIndex, 0)
-                       contactDetails = item.data().toPyObject()
+                       contactDetails = item.data()
 
-                       name = unicode(contactDetails[QtCore.QString("name")])
+                       name = unicode(contactDetails["name"])
                        if not name:
-                               name = unicode(contactDetails[QtCore.QString("location")])
+                               name = unicode(contactDetails["location"])
                        if not name:
                                name = "Unknown"
 
-                       contactId = str(contactDetails[QtCore.QString("contactId")])
-                       numbers = contactDetails[QtCore.QString("numbers")]
+                       contactId = str(contactDetails["contactId"])
+                       numbers = contactDetails["numbers"]
                        numbers = [
                                dict(
                                        (str(k), str(v))
@@ -907,7 +956,7 @@ class Contacts(object):
                        ]
                        title = name
                        description = name
-                       self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
+                       self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
 
        @staticmethod
        def _choose_phonetype(numberDetails):
index 211036e..f1b6328 100755 (executable)
@@ -3,7 +3,7 @@
 import dbus
 
 
-class LedHandler(object):
+class _NokiaLedHandler(object):
 
        def __init__(self):
                self._bus = dbus.SystemBus()
@@ -19,6 +19,48 @@ class LedHandler(object):
                self._mceRequest.req_led_pattern_deactivate(self._ledPattern)
 
 
+class _NoLedHandler(object):
+
+       def __init__(self):
+               pass
+
+       def on(self):
+               pass
+
+       def off(self):
+               pass
+
+
+class LedHandler(object):
+
+       def __init__(self):
+               self._actual = None
+               self._isReal = False
+
+       def on(self):
+               self._lazy_init()
+               self._actual.on()
+
+       def off(self):
+               self._lazy_init()
+               self._actual.on()
+
+       @property
+       def isReal(self):
+               self._lazy_init()
+               self._isReal
+
+       def _lazy_init(self):
+               if self._actual is not None:
+                       return
+               try:
+                       self._actual = _NokiaLedHandler()
+                       self._isReal = True
+               except dbus.DBusException:
+                       self._actual = _NoLedHandler()
+                       self._isReal = False
+
+
 if __name__ == "__main__":
-       leds = LedHandler()
+       leds = _NokiaLedHandler()
        leds.off()
index f0fb9cf..dbdc3e4 100644 (file)
@@ -12,7 +12,8 @@ try:
 except ImportError:
        import pickle
 
-from PyQt4 import QtCore
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
 
 from util import qore_utils
 from util import qui_utils
@@ -27,7 +28,8 @@ _moduleLogger = logging.getLogger(__name__)
 
 class _DraftContact(object):
 
-       def __init__(self, title, description, numbersWithDescriptions):
+       def __init__(self, messageId, title, description, numbersWithDescriptions):
+               self.messageId = messageId
                self.title = title
                self.description = description
                self.numbers = numbersWithDescriptions
@@ -36,21 +38,21 @@ class _DraftContact(object):
 
 class Draft(QtCore.QObject):
 
-       sendingMessage = QtCore.pyqtSignal()
-       sentMessage = QtCore.pyqtSignal()
-       calling = QtCore.pyqtSignal()
-       called = QtCore.pyqtSignal()
-       cancelling = QtCore.pyqtSignal()
-       cancelled = QtCore.pyqtSignal()
-       error = QtCore.pyqtSignal(str)
+       sendingMessage = qt_compat.Signal()
+       sentMessage = qt_compat.Signal()
+       calling = qt_compat.Signal()
+       called = qt_compat.Signal()
+       cancelling = qt_compat.Signal()
+       cancelled = qt_compat.Signal()
+       error = qt_compat.Signal(str)
 
-       recipientsChanged = QtCore.pyqtSignal()
+       recipientsChanged = qt_compat.Signal()
 
-       def __init__(self, pool, backend, errorLog):
+       def __init__(self, asyncQueue, backend, errorLog):
                QtCore.QObject.__init__(self)
                self._errorLog = errorLog
                self._contacts = {}
-               self._pool = pool
+               self._asyncQueue = asyncQueue
                self._backend = backend
                self._busyReason = None
                self._message = ""
@@ -59,7 +61,7 @@ class Draft(QtCore.QObject):
                assert 0 < len(self._contacts), "No contacts selected"
                assert 0 < len(self._message), "No message to send"
                numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
-               le = concurrent.AsyncLinearExecution(self._pool, self._send)
+               le = self._asyncQueue.add_async(self._send)
                le.start(numbers, self._message)
 
        def call(self):
@@ -67,11 +69,11 @@ class Draft(QtCore.QObject):
                assert len(self._message) == 0, "Cannot send message with call"
                (contact, ) = self._contacts.itervalues()
                number = misc_utils.make_ugly(contact.selectedNumber)
-               le = concurrent.AsyncLinearExecution(self._pool, self._call)
+               le = self._asyncQueue.add_async(self._call)
                le.start(number)
 
        def cancel(self):
-               le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
+               le = self._asyncQueue.add_async(self._cancel)
                le.start()
 
        def _get_message(self):
@@ -82,11 +84,11 @@ class Draft(QtCore.QObject):
 
        message = property(_get_message, _set_message)
 
-       def add_contact(self, contactId, title, description, numbersWithDescriptions):
+       def add_contact(self, contactId, messageId, title, description, numbersWithDescriptions):
                if self._busyReason is not None:
                        raise RuntimeError("Please wait for %r" % self._busyReason)
                # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up
-               contactDetails = _DraftContact(title, description, numbersWithDescriptions)
+               contactDetails = _DraftContact(messageId, title, description, numbersWithDescriptions)
                self._contacts[contactId] = contactDetails
                self.recipientsChanged.emit()
 
@@ -103,6 +105,9 @@ class Draft(QtCore.QObject):
        def get_num_contacts(self):
                return len(self._contacts)
 
+       def get_message_id(self, cid):
+               return self._contacts[cid].messageId
+
        def get_title(self, cid):
                return self._contacts[cid].title
 
@@ -195,41 +200,60 @@ class Session(QtCore.QObject):
        # @todo Somehow add support for csv contacts
        # @BUG When loading without caches, downloads messages twice
 
-       stateChange = QtCore.pyqtSignal(str)
-       loggedOut = QtCore.pyqtSignal()
-       loggedIn = QtCore.pyqtSignal()
-       callbackNumberChanged = QtCore.pyqtSignal(str)
+       stateChange = qt_compat.Signal(str)
+       loggedOut = qt_compat.Signal()
+       loggedIn = qt_compat.Signal()
+       callbackNumberChanged = qt_compat.Signal(str)
 
-       contactsUpdated = QtCore.pyqtSignal()
-       messagesUpdated = QtCore.pyqtSignal()
-       historyUpdated = QtCore.pyqtSignal()
-       dndStateChange = QtCore.pyqtSignal(bool)
+       accountUpdated = qt_compat.Signal()
+       messagesUpdated = qt_compat.Signal()
+       newMessages = qt_compat.Signal()
+       historyUpdated = qt_compat.Signal()
+       dndStateChange = qt_compat.Signal(bool)
+       voicemailAvailable = qt_compat.Signal(str, str)
 
-       error = QtCore.pyqtSignal(str)
+       error = qt_compat.Signal(str)
 
        LOGGEDOUT_STATE = "logged out"
        LOGGINGIN_STATE = "logging in"
        LOGGEDIN_STATE = "logged in"
 
-       _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
+       MESSAGE_TEXTS = "Text"
+       MESSAGE_VOICEMAILS = "Voicemail"
+       MESSAGE_ALL = "All"
+
+       HISTORY_RECEIVED = "Received"
+       HISTORY_MISSED = "Missed"
+       HISTORY_PLACED = "Placed"
+       HISTORY_ALL = "All"
+
+       _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.3.0")
 
        _LOGGEDOUT_TIME = -1
        _LOGGINGIN_TIME = 0
 
-       def __init__(self, errorLog, cachePath = None):
+       def __init__(self, errorLog, cachePath):
                QtCore.QObject.__init__(self)
                self._errorLog = errorLog
-               self._pool = qore_utils.AsyncPool()
+               self._pool = qore_utils.FutureThread()
+               self._asyncQueue = concurrent.AsyncTaskQueue(self._pool)
                self._backend = []
                self._loggedInTime = self._LOGGEDOUT_TIME
                self._loginOps = []
                self._cachePath = cachePath
+               self._voicemailCachePath = None
                self._username = None
-               self._draft = Draft(self._pool, self._backend, self._errorLog)
+               self._password = None
+               self._draft = Draft(self._asyncQueue, self._backend, self._errorLog)
+               self._delayedRelogin = QtCore.QTimer()
+               self._delayedRelogin.setInterval(0)
+               self._delayedRelogin.setSingleShot(True)
+               self._delayedRelogin.timeout.connect(self._on_delayed_relogin)
 
                self._contacts = {}
-               self._contactUpdateTime = datetime.datetime(1971, 1, 1)
+               self._accountUpdateTime = datetime.datetime(1971, 1, 1)
                self._messages = []
+               self._cleanMessages = []
                self._messageUpdateTime = datetime.datetime(1971, 1, 1)
                self._history = []
                self._historyUpdateTime = datetime.datetime(1971, 1, 1)
@@ -261,7 +285,7 @@ class Session(QtCore.QObject):
                        self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
 
                self._pool.start()
-               le = concurrent.AsyncLinearExecution(self._pool, self._login)
+               le = self._asyncQueue.add_async(self._login)
                le.start(username, password)
 
        def logout(self):
@@ -271,6 +295,7 @@ class Session(QtCore.QObject):
                self._loggedInTime = self._LOGGEDOUT_TIME
                self._backend[0].persist()
                self._save_to_cache()
+               self._clear_voicemail_cache()
                self.stateChange.emit(self.LOGGEDOUT_STATE)
                self.loggedOut.emit()
 
@@ -290,22 +315,26 @@ class Session(QtCore.QObject):
                self.stateChange.emit(self.LOGGEDOUT_STATE)
                self.loggedOut.emit()
 
-       def update_contacts(self, force = True):
+       def update_account(self, force = True):
                if not force and self._contacts:
                        return
-               le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
+               le = self._asyncQueue.add_async(self._update_account), (), {}
                self._perform_op_while_loggedin(le)
 
+       def refresh_connection(self):
+               le = self._asyncQueue.add_async(self._refresh_authentication)
+               le.start()
+
        def get_contacts(self):
                return self._contacts
 
        def get_when_contacts_updated(self):
-               return self._contactUpdateTime
+               return self._accountUpdateTime
 
-       def update_messages(self, force = True):
+       def update_messages(self, messageType, force = True):
                if not force and self._messages:
                        return
-               le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
+               le = self._asyncQueue.add_async(self._update_messages), (messageType, ), {}
                self._perform_op_while_loggedin(le)
 
        def get_messages(self):
@@ -314,10 +343,10 @@ class Session(QtCore.QObject):
        def get_when_messages_updated(self):
                return self._messageUpdateTime
 
-       def update_history(self, force = True):
+       def update_history(self, historyType, force = True):
                if not force and self._history:
                        return
-               le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
+               le = self._asyncQueue.add_async(self._update_history), (historyType, ), {}
                self._perform_op_while_loggedin(le)
 
        def get_history(self):
@@ -327,13 +356,27 @@ class Session(QtCore.QObject):
                return self._historyUpdateTime
 
        def update_dnd(self):
-               le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
+               le = self._asyncQueue.add_async(self._update_dnd), (), {}
                self._perform_op_while_loggedin(le)
 
        def set_dnd(self, dnd):
-               le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
+               le = self._asyncQueue.add_async(self._set_dnd)
                le.start(dnd)
 
+       def is_available(self, messageId):
+               actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
+               return os.path.exists(actualPath)
+
+       def voicemail_path(self, messageId):
+               actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
+               if not os.path.exists(actualPath):
+                       raise RuntimeError("Voicemail not available")
+               return actualPath
+
+       def download_voicemail(self, messageId):
+               le = self._asyncQueue.add_async(self._download_voicemail)
+               le.start(messageId)
+
        def _set_dnd(self, dnd):
                oldDnd = self._dnd
                try:
@@ -369,7 +412,7 @@ class Session(QtCore.QObject):
                return self._callback
 
        def set_callback_number(self, callback):
-               le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
+               le = self._asyncQueue.add_async(self._set_callback_number)
                le.start(callback)
 
        def _set_callback_number(self, callback):
@@ -394,15 +437,15 @@ class Session(QtCore.QObject):
                        self._loggedInTime = self._LOGGINGIN_TIME
                        self.stateChange.emit(self.LOGGINGIN_STATE)
                        finalState = self.LOGGEDOUT_STATE
-                       isLoggedIn = False
+                       accountData = None
                        try:
-                               if not isLoggedIn and self._backend[0].is_quick_login_possible():
-                                       isLoggedIn = yield (
-                                               self._backend[0].is_authed,
+                               if accountData is None and self._backend[0].is_quick_login_possible():
+                                       accountData = yield (
+                                               self._backend[0].refresh_account_info,
                                                (),
                                                {},
                                        )
-                                       if isLoggedIn:
+                                       if accountData is not None:
                                                _moduleLogger.info("Logged in through cookies")
                                        else:
                                                # Force a clearing of the cookies
@@ -412,70 +455,113 @@ class Session(QtCore.QObject):
                                                        {},
                                                )
 
-                               if not isLoggedIn:
-                                       isLoggedIn = yield (
+                               if accountData is None:
+                                       accountData = yield (
                                                self._backend[0].login,
                                                (username, password),
                                                {},
                                        )
-                                       if isLoggedIn:
+                                       if accountData is not None:
                                                _moduleLogger.info("Logged in through credentials")
 
-                               if isLoggedIn:
+                               if accountData is not None:
                                        self._loggedInTime = int(time.time())
                                        oldUsername = self._username
                                        self._username = username
+                                       self._password = password
                                        finalState = self.LOGGEDIN_STATE
                                        if oldUsername != self._username:
                                                needOps = not self._load()
                                        else:
                                                needOps = True
 
+                                       self._voicemailCachePath = os.path.join(self._cachePath, "%s.voicemail.cache" % self._username)
+                                       try:
+                                               os.makedirs(self._voicemailCachePath)
+                                       except OSError, e:
+                                               if e.errno != 17:
+                                                       raise
+
                                        self.loggedIn.emit()
                                        self.stateChange.emit(finalState)
                                        finalState = None # Mark it as already set
+                                       self._process_account_data(accountData)
 
                                        if needOps:
                                                loginOps = self._loginOps[:]
                                        else:
                                                loginOps = []
                                        del self._loginOps[:]
-                                       for asyncOp in loginOps:
-                                               asyncOp.start()
+                                       for asyncOp, args, kwds in loginOps:
+                                               asyncOp.start(*args, **kwds)
                                else:
                                        self._loggedInTime = self._LOGGEDOUT_TIME
                                        self.error.emit("Error logging in")
                        except Exception, e:
+                               _moduleLogger.exception("Booh")
                                self._loggedInTime = self._LOGGEDOUT_TIME
                                _moduleLogger.exception("Reporting error to user")
                                self.error.emit(str(e))
                        finally:
                                if finalState is not None:
                                        self.stateChange.emit(finalState)
-                       if isLoggedIn and self._callback:
+                       if accountData is not None and self._callback:
                                self.set_callback_number(self._callback)
 
+       def _update_account(self):
+               try:
+                       with qui_utils.notify_busy(self._errorLog, "Updating Account"):
+                               accountData = yield (
+                                       self._backend[0].refresh_account_info,
+                                       (),
+                                       {},
+                               )
+               except Exception, e:
+                       _moduleLogger.exception("Reporting error to user")
+                       self.error.emit(str(e))
+                       return
+               self._loggedInTime = int(time.time())
+               self._process_account_data(accountData)
+
+       def _refresh_authentication(self):
+               try:
+                       with qui_utils.notify_busy(self._errorLog, "Updating Account"):
+                               accountData = yield (
+                                       self._backend[0].refresh_account_info,
+                                       (),
+                                       {},
+                               )
+                               accountData = None
+               except Exception, e:
+                       _moduleLogger.exception("Passing to user")
+                       self.error.emit(str(e))
+                       # refresh_account_info does not normally throw, so it is fine if we
+                       # just quit early because something seriously wrong is going on
+                       return
+
+               if accountData is not None:
+                       self._loggedInTime = int(time.time())
+                       self._process_account_data(accountData)
+               else:
+                       self._delayedRelogin.start()
+
        def _load(self):
-               updateContacts = len(self._contacts) != 0
                updateMessages = len(self._messages) != 0
                updateHistory = len(self._history) != 0
                oldDnd = self._dnd
                oldCallback = self._callback
 
-               self._contacts = {}
                self._messages = []
+               self._cleanMessages = []
                self._history = []
                self._dnd = False
                self._callback = ""
 
                loadedFromCache = self._load_from_cache()
                if loadedFromCache:
-                       updateContacts = True
                        updateMessages = True
                        updateHistory = True
 
-               if updateContacts:
-                       self.contactsUpdated.emit()
                if updateMessages:
                        self.messagesUpdated.emit()
                if updateHistory:
@@ -495,7 +581,7 @@ class Session(QtCore.QObject):
                try:
                        with open(cachePath, "rb") as f:
                                dumpedData = pickle.load(f)
-               except (pickle.PickleError, IOError, EOFError, ValueError):
+               except (pickle.PickleError, IOError, EOFError, ValueError, ImportError):
                        _moduleLogger.exception("Pickle fun loading")
                        return False
                except:
@@ -503,27 +589,35 @@ class Session(QtCore.QObject):
                        return False
 
                try:
-                       (
-                               version, build,
-                               contacts, contactUpdateTime,
-                               messages, messageUpdateTime,
-                               history, historyUpdateTime,
-                               dnd, callback
-                       ) = dumpedData
+                       version, build = dumpedData[0:2]
                except ValueError:
                        _moduleLogger.exception("Upgrade/downgrade fun")
                        return False
                except:
                        _moduleLogger.exception("Weirdlings")
+                       return False
 
                if misc_utils.compare_versions(
                        self._OLDEST_COMPATIBLE_FORMAT_VERSION,
                        misc_utils.parse_version(version),
                ) <= 0:
+                       try:
+                               (
+                                       version, build,
+                                       messages, messageUpdateTime,
+                                       history, historyUpdateTime,
+                                       dnd, callback
+                               ) = dumpedData
+                       except ValueError:
+                               _moduleLogger.exception("Upgrade/downgrade fun")
+                               return False
+                       except:
+                               _moduleLogger.exception("Weirdlings")
+                               return False
+
                        _moduleLogger.info("Loaded cache")
-                       self._contacts = contacts
-                       self._contactUpdateTime = contactUpdateTime
                        self._messages = messages
+                       self._alert_on_messages(self._messages)
                        self._messageUpdateTime = messageUpdateTime
                        self._history = history
                        self._historyUpdateTime = historyUpdateTime
@@ -547,7 +641,6 @@ class Session(QtCore.QObject):
                try:
                        dataToDump = (
                                constants.__version__, constants.__build__,
-                               self._contacts, self._contactUpdateTime,
                                self._messages, self._messageUpdateTime,
                                self._history, self._historyUpdateTime,
                                self._dnd, self._callback
@@ -559,14 +652,11 @@ class Session(QtCore.QObject):
                        _moduleLogger.exception("While saving")
 
        def _clear_cache(self):
-               updateContacts = len(self._contacts) != 0
                updateMessages = len(self._messages) != 0
                updateHistory = len(self._history) != 0
                oldDnd = self._dnd
                oldCallback = self._callback
 
-               self._contacts = {}
-               self._contactUpdateTime = datetime.datetime(1971, 1, 1)
                self._messages = []
                self._messageUpdateTime = datetime.datetime(1971, 1, 1)
                self._history = []
@@ -574,8 +664,6 @@ class Session(QtCore.QObject):
                self._dnd = False
                self._callback = ""
 
-               if updateContacts:
-                       self.contactsUpdated.emit()
                if updateMessages:
                        self.messagesUpdated.emit()
                if updateHistory:
@@ -586,30 +674,19 @@ class Session(QtCore.QObject):
                        self.callbackNumberChanged.emit(self._callback)
 
                self._save_to_cache()
+               self._clear_voicemail_cache()
 
-       def _update_contacts(self):
-               try:
-                       assert self.state == self.LOGGEDIN_STATE, "Contacts requires being logged in (currently %s" % self.state
-                       with qui_utils.notify_busy(self._errorLog, "Updating Contacts"):
-                               self._contacts = yield (
-                                       self._backend[0].get_contacts,
-                                       (),
-                                       {},
-                               )
-               except Exception, e:
-                       _moduleLogger.exception("Reporting error to user")
-                       self.error.emit(str(e))
-                       return
-               self._contactUpdateTime = datetime.datetime.now()
-               self.contactsUpdated.emit()
+       def _clear_voicemail_cache(self):
+               import shutil
+               shutil.rmtree(self._voicemailCachePath, True)
 
-       def _update_messages(self):
+       def _update_messages(self, messageType):
                try:
                        assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state
-                       with qui_utils.notify_busy(self._errorLog, "Updating Messages"):
+                       with qui_utils.notify_busy(self._errorLog, "Updating %s Messages" % messageType):
                                self._messages = yield (
                                        self._backend[0].get_messages,
-                                       (),
+                                       (messageType, ),
                                        {},
                                )
                except Exception, e:
@@ -618,14 +695,15 @@ class Session(QtCore.QObject):
                        return
                self._messageUpdateTime = datetime.datetime.now()
                self.messagesUpdated.emit()
+               self._alert_on_messages(self._messages)
 
-       def _update_history(self):
+       def _update_history(self, historyType):
                try:
                        assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state
-                       with qui_utils.notify_busy(self._errorLog, "Updating History"):
+                       with qui_utils.notify_busy(self._errorLog, "Updating '%s' History" % historyType):
                                self._history = yield (
-                                       self._backend[0].get_recent,
-                                       (),
+                                       self._backend[0].get_call_history,
+                                       (historyType, ),
                                        {},
                                )
                except Exception, e:
@@ -636,24 +714,55 @@ class Session(QtCore.QObject):
                self.historyUpdated.emit()
 
        def _update_dnd(self):
-               oldDnd = self._dnd
-               try:
-                       assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
-                       self._dnd = yield (
-                               self._backend[0].is_dnd,
-                               (),
-                               {},
-                       )
-               except Exception, e:
-                       _moduleLogger.exception("Reporting error to user")
-                       self.error.emit(str(e))
+               with qui_utils.notify_busy(self._errorLog, "Updating Do-Not-Disturb Status"):
+                       oldDnd = self._dnd
+                       try:
+                               assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
+                               self._dnd = yield (
+                                       self._backend[0].is_dnd,
+                                       (),
+                                       {},
+                               )
+                       except Exception, e:
+                               _moduleLogger.exception("Reporting error to user")
+                               self.error.emit(str(e))
+                               return
+                       if oldDnd != self._dnd:
+                               self.dndStateChange(self._dnd)
+
+       def _download_voicemail(self, messageId):
+               actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
+               targetPath = "%s.%s.part" % (actualPath, time.time())
+               if os.path.exists(actualPath):
+                       self.voicemailAvailable.emit(messageId, actualPath)
                        return
-               if oldDnd != self._dnd:
-                       self.dndStateChange(self._dnd)
+               with qui_utils.notify_busy(self._errorLog, "Downloading Voicemail"):
+                       try:
+                               yield (
+                                       self._backend[0].download,
+                                       (messageId, targetPath),
+                                       {},
+                               )
+                       except Exception, e:
+                               _moduleLogger.exception("Passing to user")
+                               self.error.emit(str(e))
+                               return
+
+               if os.path.exists(actualPath):
+                       try:
+                               os.remove(targetPath)
+                       except:
+                               _moduleLogger.exception("Ignoring file problems with cache")
+                       self.voicemailAvailable.emit(messageId, actualPath)
+                       return
+               else:
+                       os.rename(targetPath, actualPath)
+                       self.voicemailAvailable.emit(messageId, actualPath)
 
        def _perform_op_while_loggedin(self, op):
                if self.state == self.LOGGEDIN_STATE:
-                       op.start()
+                       op, args, kwds = op
+                       op.start(*args, **kwds)
                else:
                        self._push_login_op(op)
 
@@ -663,3 +772,59 @@ class Session(QtCore.QObject):
                        _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
                        return
                self._loginOps.append(asyncOp)
+
+       def _process_account_data(self, accountData):
+               self._contacts = dict(
+                       (contactId, contactDetails)
+                       for contactId, contactDetails in accountData["contacts"].iteritems()
+                       # A zero contact id is the catch all for unknown contacts
+                       if contactId != "0"
+               )
+
+               self._accountUpdateTime = datetime.datetime.now()
+               self.accountUpdated.emit()
+
+       def _alert_on_messages(self, messages):
+               cleanNewMessages = list(self._clean_messages(messages))
+               cleanNewMessages.sort(key=lambda m: m["contactId"])
+               if self._cleanMessages:
+                       if self._cleanMessages != cleanNewMessages:
+                               self.newMessages.emit()
+               self._cleanMessages = cleanNewMessages
+
+       def _clean_messages(self, messages):
+               for message in messages:
+                       cleaned = dict(
+                               kv
+                               for kv in message.iteritems()
+                               if kv[0] not in
+                               [
+                                       "relTime",
+                                       "time",
+                                       "isArchived",
+                                       "isRead",
+                                       "isSpam",
+                                       "isTrash",
+                               ]
+                       )
+
+                       # Don't let outbound messages cause alerts, especially if the package has only outbound
+                       cleaned["messageParts"] = [
+                               tuple(part[0:-1]) for part in cleaned["messageParts"] if part[0] != "Me:"
+                       ]
+                       if not cleaned["messageParts"]:
+                               continue
+
+                       yield cleaned
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_delayed_relogin(self):
+               try:
+                       username = self._username
+                       password = self._password
+                       self.logout()
+                       self.login(username, password)
+               except Exception, e:
+                       _moduleLogger.exception("Passing to user")
+                       self.error.emit(str(e))
+                       return
diff --git a/src/stream_gst.py b/src/stream_gst.py
new file mode 100644 (file)
index 0000000..ce97fb6
--- /dev/null
@@ -0,0 +1,145 @@
+import logging
+
+import gobject
+import gst
+
+import util.misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Stream(gobject.GObject):
+
+       # @bug Advertising state changes a bit early, should watch for GStreamer state change
+
+       STATE_PLAY = "play"
+       STATE_PAUSE = "pause"
+       STATE_STOP = "stop"
+
+       __gsignals__ = {
+               'state-change' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_STRING, ),
+               ),
+               'eof' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_STRING, ),
+               ),
+               'error' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT),
+               ),
+       }
+
+       def __init__(self):
+               gobject.GObject.__init__(self)
+               #Fields
+               self._uri = ""
+               self._elapsed = 0
+               self._duration = 0
+
+               #Set up GStreamer
+               self._player = gst.element_factory_make("playbin2", "player")
+               bus = self._player.get_bus()
+               bus.add_signal_watch()
+               bus.connect("message", self._on_message)
+
+               #Constants
+               self._timeFormat = gst.Format(gst.FORMAT_TIME)
+               self._seekFlag = gst.SEEK_FLAG_FLUSH
+
+       @property
+       def playing(self):
+               return self.state == self.STATE_PLAY
+
+       @property
+       def has_file(self):
+               return 0 < len(self._uri)
+
+       @property
+       def state(self):
+               state = self._player.get_state()[1]
+               return self._translate_state(state)
+
+       def set_file(self, uri):
+               if self._uri != uri:
+                       self._invalidate_cache()
+               if self.state != self.STATE_STOP:
+                       self.stop()
+
+               self._uri = uri
+               self._player.set_property("uri", uri)
+
+       def play(self):
+               if self.state == self.STATE_PLAY:
+                       _moduleLogger.info("Already play")
+                       return
+               _moduleLogger.info("Play")
+               self._player.set_state(gst.STATE_PLAYING)
+               self.emit("state-change", self.STATE_PLAY)
+
+       def pause(self):
+               if self.state == self.STATE_PAUSE:
+                       _moduleLogger.info("Already pause")
+                       return
+               _moduleLogger.info("Pause")
+               self._player.set_state(gst.STATE_PAUSED)
+               self.emit("state-change", self.STATE_PAUSE)
+
+       def stop(self):
+               if self.state == self.STATE_STOP:
+                       _moduleLogger.info("Already stop")
+                       return
+               self._player.set_state(gst.STATE_NULL)
+               _moduleLogger.info("Stopped")
+               self.emit("state-change", self.STATE_STOP)
+
+       @property
+       def elapsed(self):
+               try:
+                       self._elapsed = self._player.query_position(self._timeFormat, None)[0]
+               except:
+                       pass
+               return self._elapsed
+
+       @property
+       def duration(self):
+               try:
+                       self._duration = self._player.query_duration(self._timeFormat, None)[0]
+               except:
+                       _moduleLogger.exception("Query failed")
+               return self._duration
+
+       def seek_time(self, ns):
+               self._elapsed = ns
+               self._player.seek_simple(self._timeFormat, self._seekFlag, ns)
+
+       def _invalidate_cache(self):
+               self._elapsed = 0
+               self._duration = 0
+
+       def _translate_state(self, gstState):
+               return {
+                       gst.STATE_NULL: self.STATE_STOP,
+                       gst.STATE_PAUSED: self.STATE_PAUSE,
+                       gst.STATE_PLAYING: self.STATE_PLAY,
+               }.get(gstState, self.STATE_STOP)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_message(self, bus, message):
+               t = message.type
+               if t == gst.MESSAGE_EOS:
+                       self._player.set_state(gst.STATE_NULL)
+                       self.emit("eof", self._uri)
+               elif t == gst.MESSAGE_ERROR:
+                       self._player.set_state(gst.STATE_NULL)
+                       err, debug = message.parse_error()
+                       _moduleLogger.error("Error: %s, (%s)" % (err, debug))
+                       self.emit("error", err, debug)
+
+
+gobject.type_register(Stream)
diff --git a/src/stream_handler.py b/src/stream_handler.py
new file mode 100644 (file)
index 0000000..3c0c9e3
--- /dev/null
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+
+import util.misc as misc_utils
+try:
+       import stream_gst
+       stream = stream_gst
+except ImportError:
+       try:
+               import stream_osso
+               stream = stream_osso
+       except ImportError:
+               import stream_null
+               stream = stream_null
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class StreamToken(QtCore.QObject):
+
+       stateChange = qt_compat.Signal(str)
+       invalidated = qt_compat.Signal()
+       error = qt_compat.Signal(str)
+
+       STATE_PLAY = stream.Stream.STATE_PLAY
+       STATE_PAUSE = stream.Stream.STATE_PAUSE
+       STATE_STOP = stream.Stream.STATE_STOP
+
+       def __init__(self, stream):
+               QtCore.QObject.__init__(self)
+               self._stream = stream
+               self._stream.connect("state-change", self._on_stream_state)
+               self._stream.connect("eof", self._on_stream_eof)
+               self._stream.connect("error", self._on_stream_error)
+
+       @property
+       def state(self):
+               if self.isValid:
+                       return self._stream.state
+               else:
+                       return self.STATE_STOP
+
+       @property
+       def isValid(self):
+               return self._stream is not None
+
+       def play(self):
+               self._stream.play()
+
+       def pause(self):
+               self._stream.pause()
+
+       def stop(self):
+               self._stream.stop()
+
+       def invalidate(self):
+               if self._stream is None:
+                       return
+               _moduleLogger.info("Playback token invalidated")
+               self._stream = None
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_stream_state(self, s, state):
+               if not self.isValid:
+                       return
+               if state == self.STATE_STOP:
+                       self.invalidate()
+               self.stateChange.emit(state)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_stream_eof(self, s, uri):
+               if not self.isValid:
+                       return
+               self.invalidate()
+               self.stateChange.emit(self.STATE_STOP)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_stream_error(self, s, error, debug):
+               if not self.isValid:
+                       return
+               _moduleLogger.info("Error %s %s" % (error, debug))
+               self.error.emit(str(error))
+
+
+class StreamHandler(QtCore.QObject):
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+               self._stream = stream.Stream()
+               self._token = StreamToken(self._stream)
+
+       def set_file(self, path):
+               self._token.invalidate()
+               self._token = StreamToken(self._stream)
+               self._stream.set_file(path)
+               return self._token
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_stream_state(self, s, state):
+               _moduleLogger.info("State change %r" % state)
+
+
+if __name__ == "__main__":
+       pass
+
diff --git a/src/stream_null.py b/src/stream_null.py
new file mode 100644 (file)
index 0000000..44fbbed
--- /dev/null
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Stream(object):
+
+       STATE_PLAY = "play"
+       STATE_PAUSE = "pause"
+       STATE_STOP = "stop"
+
+       def __init__(self):
+               pass
+
+       def connect(self, signalName, slot):
+               pass
+
+       @property
+       def playing(self):
+               return False
+
+       @property
+       def has_file(self):
+               return False
+
+       @property
+       def state(self):
+               return self.STATE_STOP
+
+       def set_file(self, uri):
+               pass
+
+       def play(self):
+               pass
+
+       def pause(self):
+               pass
+
+       def stop(self):
+               pass
+
+       @property
+       def elapsed(self):
+               return 0
+
+       @property
+       def duration(self):
+               return 0
+
+       def seek_time(self, ns):
+               pass
+
+
+if __name__ == "__main__":
+       pass
+
diff --git a/src/stream_osso.py b/src/stream_osso.py
new file mode 100644 (file)
index 0000000..abc453f
--- /dev/null
@@ -0,0 +1,181 @@
+import logging
+
+import gobject
+import dbus
+
+import util.misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Stream(gobject.GObject):
+
+       STATE_PLAY = "play"
+       STATE_PAUSE = "pause"
+       STATE_STOP = "stop"
+
+       __gsignals__ = {
+               'state-change' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_STRING, ),
+               ),
+               'eof' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_STRING, ),
+               ),
+               'error' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT),
+               ),
+       }
+
+       _SERVICE_NAME = "com.nokia.osso_media_server"
+       _OBJECT_PATH = "/com/nokia/osso_media_server"
+       _AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music"
+
+       def __init__(self):
+               gobject.GObject.__init__(self)
+               #Fields
+               self._state = self.STATE_STOP
+               self._nextState = self.STATE_STOP
+               self._uri = ""
+               self._elapsed = 0
+               self._duration = 0
+
+               session_bus = dbus.SessionBus()
+
+               # Get the osso-media-player proxy object
+               oms_object = session_bus.get_object(
+                       self._SERVICE_NAME,
+                       self._OBJECT_PATH,
+                       introspect=False,
+                       follow_name_owner_changes=True,
+               )
+               # Use the audio interface
+               oms_audio_interface = dbus.Interface(
+                       oms_object,
+                       self._AUDIO_INTERFACE_NAME,
+               )
+               self._audioProxy = oms_audio_interface
+
+               self._audioProxy.connect_to_signal("state_changed", self._on_state_changed)
+               self._audioProxy.connect_to_signal("end_of_stream", self._on_end_of_stream)
+
+               error_signals = [
+                       "no_media_selected",
+                       "file_not_found",
+                       "type_not_found",
+                       "unsupported_type",
+                       "gstreamer",
+                       "dsp",
+                       "device_unavailable",
+                       "corrupted_file",
+                       "out_of_memory",
+                       "audio_codec_not_supported",
+               ]
+               for error in error_signals:
+                       self._audioProxy.connect_to_signal(error, self._on_error)
+
+       @property
+       def playing(self):
+               return self.state == self.STATE_PLAY
+
+       @property
+       def has_file(self):
+               return 0 < len(self._uri)
+
+       @property
+       def state(self):
+               return self._state
+
+       def set_file(self, uri):
+               if self._uri != uri:
+                       self._invalidate_cache()
+               if self.state != self.STATE_STOP:
+                       self.stop()
+
+               self._uri = uri
+               self._audioProxy.set_media_location(self._uri)
+
+       def play(self):
+               if self._nextState == self.STATE_PLAY:
+                       _moduleLogger.info("Already play")
+                       return
+               _moduleLogger.info("Play")
+               self._audioProxy.play()
+               self._nextState = self.STATE_PLAY
+               #self.emit("state-change", self.STATE_PLAY)
+
+       def pause(self):
+               if self._nextState == self.STATE_PAUSE:
+                       _moduleLogger.info("Already pause")
+                       return
+               _moduleLogger.info("Pause")
+               self._audioProxy.pause()
+               self._nextState = self.STATE_PAUSE
+               #self.emit("state-change", self.STATE_PLAY)
+
+       def stop(self):
+               if self._nextState == self.STATE_STOP:
+                       _moduleLogger.info("Already stop")
+                       return
+               self._audioProxy.stop()
+               _moduleLogger.info("Stopped")
+               self._nextState = self.STATE_STOP
+               #self.emit("state-change", self.STATE_STOP)
+
+       @property
+       def elapsed(self):
+               pos_info = self._audioProxy.get_position()
+               if isinstance(pos_info, tuple):
+                       self._elapsed, self._duration = pos_info
+               return self._elapsed
+
+       @property
+       def duration(self):
+               pos_info = self._audioProxy.get_position()
+               if isinstance(pos_info, tuple):
+                       self._elapsed, self._duration = pos_info
+               return self._duration
+
+       def seek_time(self, ns):
+               _moduleLogger.debug("Seeking to: %s", ns)
+               self._audioProxy.seek( dbus.Int32(1), dbus.Int32(ns) )
+
+       def _invalidate_cache(self):
+               self._elapsed = 0
+               self._duration = 0
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_error(self, *args):
+               err, debug = "", repr(args)
+               _moduleLogger.error("Error: %s, (%s)" % (err, debug))
+               self.emit("error", err, debug)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_end_of_stream(self, *args):
+               self._state = self.STATE_STOP
+               self._nextState = self.STATE_STOP
+               self.emit("eof", self._uri)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_state_changed(self, state):
+               _moduleLogger.info("State: %s", state)
+               state = {
+                       "playing": self.STATE_PLAY,
+                       "paused": self.STATE_PAUSE,
+                       "stopped": self.STATE_STOP,
+               }[state]
+               if self._state == self.STATE_STOP and self._nextState == self.STATE_PLAY and state == self.STATE_STOP:
+                       # They seem to want to advertise stop right as the stream is starting, breaking the owner of this
+                       return
+               self._state = state
+               self._nextState = state
+               self.emit("state-change", state)
+
+
+gobject.type_register(Stream)
index a6499fe..4d31468 100644 (file)
@@ -15,12 +15,33 @@ import misc
 _moduleLogger = logging.getLogger(__name__)
 
 
-class AsyncLinearExecution(object):
+class AsyncTaskQueue(object):
+
+       def __init__(self, taskPool):
+               self._asyncs = []
+               self._taskPool = taskPool
+
+       def add_async(self, func):
+               self.flush()
+               a = _AsyncGeneratorTask(self._taskPool, func)
+               self._asyncs.append(a)
+               return a
+
+       def flush(self):
+               self._asyncs = [a for a in self._asyncs if not a.isDone]
+
+
+class _AsyncGeneratorTask(object):
 
        def __init__(self, pool, func):
                self._pool = pool
                self._func = func
                self._run = None
+               self._isDone = False
+
+       @property
+       def isDone(self):
+               return self._isDone
 
        def start(self, *args, **kwds):
                assert self._run is None, "Task already started"
@@ -40,7 +61,7 @@ class AsyncLinearExecution(object):
                try:
                        trampoline, args, kwds = self._run.send(result)
                except StopIteration, e:
-                       pass
+                       self._isDone = True
                else:
                        self._pool.add_task(
                                trampoline,
@@ -56,7 +77,7 @@ class AsyncLinearExecution(object):
                try:
                        trampoline, args, kwds = self._run.throw(error)
                except StopIteration, e:
-                       pass
+                       self._isDone = True
                else:
                        self._pool.add_task(
                                trampoline,
index eaa2fe1..61e731d 100644 (file)
@@ -138,7 +138,7 @@ class Timeout(object):
 _QUEUE_EMPTY = object()
 
 
-class AsyncPool(object):
+class FutureThread(object):
 
        def __init__(self):
                self.__workQueue = Queue.Queue()
index c86cfbc..153558d 100644 (file)
@@ -1,6 +1,7 @@
 import logging
 
-from PyQt4 import QtCore
+import qt_compat
+QtCore = qt_compat.QtCore
 
 import misc
 
@@ -22,40 +23,23 @@ class QThread44(QtCore.QThread):
                self.exec_()
 
 
-class _ParentThread(QtCore.QObject):
-
-       def __init__(self, pool):
-               QtCore.QObject.__init__(self)
-               self._pool = pool
-
-       @QtCore.pyqtSlot(object)
-       @misc.log_exception(_moduleLogger)
-       def _on_task_complete(self, taskResult):
-               on_success, on_error, isError, result = taskResult
-               if not self._pool._isRunning:
-                       if isError:
-                               _moduleLogger.error("Masking: %s" % (result, ))
-                       isError = True
-                       result = StopIteration("Cancelling all callbacks")
-               callback = on_success if not isError else on_error
-               try:
-                       callback(result)
-               except Exception:
-                       _moduleLogger.exception("Callback errored")
-
-
 class _WorkerThread(QtCore.QObject):
 
-       taskComplete  = QtCore.pyqtSignal(object)
+       _taskComplete  = qt_compat.Signal(object)
 
-       def __init__(self, pool):
+       def __init__(self, futureThread):
                QtCore.QObject.__init__(self)
-               self._pool = pool
+               self._futureThread = futureThread
+               self._futureThread._addTask.connect(self._on_task_added)
+               self._taskComplete.connect(self._futureThread._on_task_complete)
 
-       @QtCore.pyqtSlot(object)
-       @misc.log_exception(_moduleLogger)
+       @qt_compat.Slot(object)
        def _on_task_added(self, task):
-               if not self._pool._isRunning:
+               self.__on_task_added(task)
+
+       @misc.log_exception(_moduleLogger)
+       def __on_task_added(self, task):
+               if not self._futureThread._isRunning:
                        _moduleLogger.error("Dropping task")
 
                func, args, kwds, on_success, on_error = task
@@ -69,40 +53,47 @@ class _WorkerThread(QtCore.QObject):
                        isError = True
 
                taskResult = on_success, on_error, isError, result
-               self.taskComplete.emit(taskResult)
-
-       @QtCore.pyqtSlot()
-       @misc.log_exception(_moduleLogger)
-       def _on_stop_requested(self):
-               self._pool._thread.quit()
+               self._taskComplete.emit(taskResult)
 
 
-class AsyncPool(QtCore.QObject):
+class FutureThread(QtCore.QObject):
 
-       _addTask = QtCore.pyqtSignal(object)
-       _stopPool = QtCore.pyqtSignal()
+       _addTask = qt_compat.Signal(object)
 
        def __init__(self):
                QtCore.QObject.__init__(self)
                self._thread = QThread44()
                self._isRunning = False
-               self._parent = _ParentThread(self)
                self._worker = _WorkerThread(self)
                self._worker.moveToThread(self._thread)
 
-               self._addTask.connect(self._worker._on_task_added)
-               self._worker.taskComplete.connect(self._parent._on_task_complete)
-               self._stopPool.connect(self._worker._on_stop_requested)
-
        def start(self):
                self._thread.start()
                self._isRunning = True
 
        def stop(self):
                self._isRunning = False
-               self._stopPool.emit()
+               self._thread.quit()
 
        def add_task(self, func, args, kwds, on_success, on_error):
                assert self._isRunning, "Task queue not started"
                task = func, args, kwds, on_success, on_error
                self._addTask.emit(task)
+
+       @qt_compat.Slot(object)
+       def _on_task_complete(self, taskResult):
+               self.__on_task_complete(taskResult)
+
+       @misc.log_exception(_moduleLogger)
+       def __on_task_complete(self, taskResult):
+               on_success, on_error, isError, result = taskResult
+               if not self._isRunning:
+                       if isError:
+                               _moduleLogger.error("Masking: %s" % (result, ))
+                       isError = True
+                       result = StopIteration("Cancelling all callbacks")
+               callback = on_success if not isError else on_error
+               try:
+                       callback(result)
+               except Exception:
+                       _moduleLogger.exception("Callback errored")
diff --git a/src/util/qt_compat.py b/src/util/qt_compat.py
new file mode 100644 (file)
index 0000000..3ab17c8
--- /dev/null
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+try:
+       import PySide.QtCore as _QtCore
+       QtCore = _QtCore
+       USES_PYSIDE = True
+except ImportError:
+       import sip
+       sip.setapi('QString', 2)
+       sip.setapi('QVariant', 2)
+       import PyQt4.QtCore as _QtCore
+       QtCore = _QtCore
+       USES_PYSIDE = False
+
+
+def _pyside_import_module(moduleName):
+       pyside = __import__('PySide', globals(), locals(), [moduleName], -1)
+       return getattr(pyside, moduleName)
+
+
+def _pyqt4_import_module(moduleName):
+       pyside = __import__('PyQt4', globals(), locals(), [moduleName], -1)
+       return getattr(pyside, moduleName)
+
+
+if USES_PYSIDE:
+       import_module = _pyside_import_module
+
+       Signal = QtCore.Signal
+       Slot = QtCore.Slot
+       Property = QtCore.Property
+else:
+       import_module = _pyqt4_import_module
+
+       Signal = QtCore.pyqtSignal
+       Slot = QtCore.pyqtSlot
+       Property = QtCore.pyqtProperty
+
+
+if __name__ == "__main__":
+       pass
+
index c23e512..6b77d5d 100755 (executable)
@@ -3,8 +3,9 @@
 import math
 import logging
 
-from PyQt4 import QtGui
-from PyQt4 import QtCore
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
 
 import misc as misc_utils
 
@@ -496,11 +497,11 @@ class QPieDisplay(QtGui.QWidget):
 
 class QPieButton(QtGui.QWidget):
 
-       activated = QtCore.pyqtSignal(int)
-       highlighted = QtCore.pyqtSignal(int)
-       canceled = QtCore.pyqtSignal()
-       aboutToShow = QtCore.pyqtSignal()
-       aboutToHide = QtCore.pyqtSignal()
+       activated = qt_compat.Signal(int)
+       highlighted = qt_compat.Signal(int)
+       canceled = qt_compat.Signal()
+       aboutToShow = qt_compat.Signal()
+       aboutToHide = qt_compat.Signal()
 
        BUTTON_RADIUS = 24
        DELAY = 250
@@ -778,11 +779,11 @@ class QPieButton(QtGui.QWidget):
 
 class QPieMenu(QtGui.QWidget):
 
-       activated = QtCore.pyqtSignal(int)
-       highlighted = QtCore.pyqtSignal(int)
-       canceled = QtCore.pyqtSignal()
-       aboutToShow = QtCore.pyqtSignal()
-       aboutToHide = QtCore.pyqtSignal()
+       activated = qt_compat.Signal(int)
+       highlighted = qt_compat.Signal(int)
+       canceled = qt_compat.Signal()
+       aboutToShow = qt_compat.Signal()
+       aboutToHide = qt_compat.Signal()
 
        def __init__(self, parent = None):
                QtGui.QWidget.__init__(self, parent)
index c7094f4..50ae9ae 100755 (executable)
@@ -6,7 +6,8 @@ from __future__ import division
 import os
 import warnings
 
-from PyQt4 import QtGui
+import qt_compat
+QtGui = qt_compat.import_module("QtGui")
 
 import qtpie
 
index 9e33f88..ac43ee6 100644 (file)
@@ -3,8 +3,9 @@ import contextlib
 import datetime
 import logging
 
-from PyQt4 import QtCore
-from PyQt4 import QtGui
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
 
 import misc
 
@@ -54,8 +55,8 @@ class ErrorMessage(object):
 
 class QErrorLog(QtCore.QObject):
 
-       messagePushed = QtCore.pyqtSignal()
-       messagePopped = QtCore.pyqtSignal()
+       messagePushed = qt_compat.Signal()
+       messagePopped = qt_compat.Signal()
 
        def __init__(self):
                QtCore.QObject.__init__(self)
@@ -113,21 +114,7 @@ class ErrorDisplay(object):
                self._errorLog.messagePushed.connect(self._on_message_pushed)
                self._errorLog.messagePopped.connect(self._on_message_popped)
 
-               self._icons = {
-                       ErrorMessage.LEVEL_BUSY:
-                               get_theme_icon(
-                                       #("process-working", "view-refresh", "general_refresh", "gtk-refresh")
-                                       ("view-refresh", "general_refresh", "gtk-refresh", )
-                               ).pixmap(32, 32),
-                       ErrorMessage.LEVEL_INFO:
-                               get_theme_icon(
-                                       ("dialog-information", "general_notes", "gtk-info")
-                               ).pixmap(32, 32),
-                       ErrorMessage.LEVEL_ERROR:
-                               get_theme_icon(
-                                       ("dialog-error", "app_install_error", "gtk-dialog-error")
-                               ).pixmap(32, 32),
-               }
+               self._icons = None
                self._severityLabel = QtGui.QLabel()
                self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
 
@@ -136,17 +123,11 @@ class ErrorDisplay(object):
                self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
                self._message.setWordWrap(True)
 
-               closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
-               if closeIcon is not self._SENTINEL_ICON:
-                       self._closeLabel = QtGui.QPushButton(closeIcon, "")
-               else:
-                       self._closeLabel = QtGui.QPushButton("X")
-               self._closeLabel.clicked.connect(self._on_close)
+               self._closeLabel = None
 
                self._controlLayout = QtGui.QHBoxLayout()
                self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter)
                self._controlLayout.addWidget(self._message, 1000)
-               self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
 
                self._widget = QtGui.QWidget()
                self._widget.setLayout(self._controlLayout)
@@ -157,23 +138,47 @@ class ErrorDisplay(object):
                return self._widget
 
        def _show_error(self):
+               if self._icons is None:
+                       self._icons = {
+                               ErrorMessage.LEVEL_BUSY:
+                                       get_theme_icon(
+                                               #("process-working", "view-refresh", "general_refresh", "gtk-refresh")
+                                               ("view-refresh", "general_refresh", "gtk-refresh", )
+                                       ).pixmap(32, 32),
+                               ErrorMessage.LEVEL_INFO:
+                                       get_theme_icon(
+                                               ("dialog-information", "general_notes", "gtk-info")
+                                       ).pixmap(32, 32),
+                               ErrorMessage.LEVEL_ERROR:
+                                       get_theme_icon(
+                                               ("dialog-error", "app_install_error", "gtk-dialog-error")
+                                       ).pixmap(32, 32),
+                       }
+               if self._closeLabel is None:
+                       closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
+                       if closeIcon is not self._SENTINEL_ICON:
+                               self._closeLabel = QtGui.QPushButton(closeIcon, "")
+                       else:
+                               self._closeLabel = QtGui.QPushButton("X")
+                       self._closeLabel.clicked.connect(self._on_close)
+                       self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
                error = self._errorLog.peek_message()
                self._message.setText(error.message)
                self._severityLabel.setPixmap(self._icons[error.level])
                self._widget.show()
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc.log_exception(_moduleLogger)
        def _on_close(self, checked = False):
                self._errorLog.pop()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc.log_exception(_moduleLogger)
        def _on_message_pushed(self):
                self._show_error()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc.log_exception(_moduleLogger)
        def _on_message_popped(self):
                if len(self._errorLog) == 0:
@@ -260,24 +265,41 @@ class QHtmlDelegate(QtGui.QStyledItemDelegate):
 
 class QSignalingMainWindow(QtGui.QMainWindow):
 
-       closed = QtCore.pyqtSignal()
-       hidden = QtCore.pyqtSignal()
-       shown = QtCore.pyqtSignal()
+       closed = qt_compat.Signal()
+       hidden = qt_compat.Signal()
+       shown = qt_compat.Signal()
+       resized = qt_compat.Signal()
 
        def __init__(self, *args, **kwd):
                QtGui.QMainWindow.__init__(*((self, )+args), **kwd)
 
        def closeEvent(self, event):
-               QtGui.QMainWindow.closeEvent(self, event)
+               val = QtGui.QMainWindow.closeEvent(self, event)
                self.closed.emit()
+               return val
 
        def hideEvent(self, event):
-               QtGui.QMainWindow.hideEvent(self, event)
+               val = QtGui.QMainWindow.hideEvent(self, event)
                self.hidden.emit()
+               return val
 
        def showEvent(self, event):
-               QtGui.QMainWindow.showEvent(self, event)
+               val = QtGui.QMainWindow.showEvent(self, event)
                self.shown.emit()
+               return val
+
+       def resizeEvent(self, event):
+               val = QtGui.QMainWindow.resizeEvent(self, event)
+               self.resized.emit()
+               return val
+
+def set_current_index(selector, itemText, default = 0):
+       for i in xrange(selector.count()):
+               if selector.itemText(i) == itemText:
+                       selector.setCurrentIndex(i)
+                       break
+       else:
+               itemText.setCurrentIndex(default)
 
 
 def _null_set_stackable(window, isStackable):
@@ -295,12 +317,12 @@ except AttributeError:
        set_stackable = _null_set_stackable
 
 
-def _null_set_autorient(window, isStackable):
+def _null_set_autorient(window, doAutoOrient):
        pass
 
 
-def _maemo_set_autorient(window, isStackable):
-       window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, isStackable)
+def _maemo_set_autorient(window, doAutoOrient):
+       window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, doAutoOrient)
 
 
 try:
@@ -324,13 +346,16 @@ def _null_set_window_orientation(window, orientation):
 
 def _maemo_set_window_orientation(window, orientation):
        if orientation == QtCore.Qt.Vertical:
-               oldHint = QtCore.Qt.WA_Maemo5LandscapeOrientation
-               newHint = QtCore.Qt.WA_Maemo5PortraitOrientation
+               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False)
+               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, True)
        elif orientation == QtCore.Qt.Horizontal:
-               oldHint = QtCore.Qt.WA_Maemo5PortraitOrientation
-               newHint = QtCore.Qt.WA_Maemo5LandscapeOrientation
-       window.setAttribute(oldHint, False)
-       window.setAttribute(newHint, True)
+               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, True)
+               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False)
+       elif orientation is None:
+               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, True)
+               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, True)
+       else:
+               raise RuntimeError("Unknown orientation: %r" % orientation)
 
 
 try:
index 2add98c..65ec755 100644 (file)
@@ -5,8 +5,9 @@ from __future__ import division
 
 import logging
 
-from PyQt4 import QtGui
-from PyQt4 import QtCore
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
 
 from util import qui_utils
 from util import misc as misc_utils
@@ -17,6 +18,11 @@ _moduleLogger = logging.getLogger(__name__)
 
 class ApplicationWrapper(object):
 
+       DEFAULT_ORIENTATION = "Default"
+       AUTO_ORIENTATION = "Auto"
+       LANDSCAPE_ORIENTATION = "Landscape"
+       PORTRAIT_ORIENTATION = "Portrait"
+
        def __init__(self, qapp, constants):
                self._constants = constants
                self._qapp = qapp
@@ -31,11 +37,12 @@ class ApplicationWrapper(object):
                self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
                self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
 
+               self._orientation = self.DEFAULT_ORIENTATION
                self._orientationAction = QtGui.QAction(None)
-               self._orientationAction.setText("Orientation")
+               self._orientationAction.setText("Next Orientation")
                self._orientationAction.setCheckable(True)
                self._orientationAction.setShortcut(QtGui.QKeySequence("CTRL+o"))
-               self._orientationAction.toggled.connect(self._on_toggle_orientation)
+               self._orientationAction.triggered.connect(self._on_next_orientation)
 
                self._logAction = QtGui.QAction(None)
                self._logAction.setText("Log")
@@ -61,7 +68,7 @@ class ApplicationWrapper(object):
                self._idleDelay = QtCore.QTimer()
                self._idleDelay.setSingleShot(True)
                self._idleDelay.setInterval(0)
-               self._idleDelay.timeout.connect(lambda: self._mainWindow.start())
+               self._idleDelay.timeout.connect(self._on_delayed_start)
                self._idleDelay.start()
 
        def load_settings(self):
@@ -94,6 +101,10 @@ class ApplicationWrapper(object):
                return self._orientationAction
 
        @property
+       def orientation(self):
+               return self._orientation
+
+       @property
        def logAction(self):
                return self._logAction
 
@@ -105,6 +116,19 @@ class ApplicationWrapper(object):
        def quitAction(self):
                return self._quitAction
 
+       def set_orientation(self, orientation):
+               self._orientation = orientation
+               self._mainWindow.update_orientation(self._orientation)
+
+       @classmethod
+       def _next_orientation(cls, current):
+               return {
+                       cls.DEFAULT_ORIENTATION: cls.AUTO_ORIENTATION,
+                       cls.AUTO_ORIENTATION: cls.LANDSCAPE_ORIENTATION,
+                       cls.LANDSCAPE_ORIENTATION: cls.PORTRAIT_ORIENTATION,
+                       cls.PORTRAIT_ORIENTATION: cls.DEFAULT_ORIENTATION,
+               }[current]
+
        def _close_windows(self):
                if self._mainWindow is not None:
                        self.save_settings()
@@ -113,6 +137,10 @@ class ApplicationWrapper(object):
                        self._mainWindow = None
 
        @misc_utils.log_exception(_moduleLogger)
+       def _on_delayed_start(self):
+               self._mainWindow.start()
+
+       @misc_utils.log_exception(_moduleLogger)
        def _on_app_quit(self, checked = False):
                if self._mainWindow is not None:
                        self.save_settings()
@@ -130,9 +158,9 @@ class ApplicationWrapper(object):
                        self._mainWindow.set_fullscreen(checked)
 
        @misc_utils.log_exception(_moduleLogger)
-       def _on_toggle_orientation(self, checked = False):
+       def _on_next_orientation(self, checked = False):
                with qui_utils.notify_error(self._errorLog):
-                       self._mainWindow.set_orientation(checked)
+                       self.set_orientation(self._next_orientation(self._orientation))
 
        @misc_utils.log_exception(_moduleLogger)
        def _on_about(self, checked = True):
@@ -191,6 +219,24 @@ class WindowWrapper(object):
        def window(self):
                return self._window
 
+       @property
+       def windowOrientation(self):
+               geom = self._window.size()
+               if geom.width() <= geom.height():
+                       return QtCore.Qt.Vertical
+               else:
+                       return QtCore.Qt.Horizontal
+
+       @property
+       def idealWindowOrientation(self):
+               if self._app.orientation ==  self._app.LANDSCAPE_ORIENTATION:
+                       windowOrientation = QtCore.Qt.Horizontal
+               elif self._app.orientation ==  self._app.PORTRAIT_ORIENTATION:
+                       windowOrientation = QtCore.Qt.Vertical
+               else:
+                       windowOrientation = self.windowOrientation
+               return windowOrientation
+
        def walk_children(self):
                return ()
 
@@ -226,13 +272,23 @@ class WindowWrapper(object):
                for child in self.walk_children():
                        child.set_fullscreen(isFullscreen)
 
-       def set_orientation(self, isPortrait):
-               if isPortrait:
+       def update_orientation(self, orientation):
+               if orientation == self._app.DEFAULT_ORIENTATION:
+                       qui_utils.set_autorient(self.window, False)
+                       qui_utils.set_window_orientation(self.window, None)
+               elif orientation == self._app.AUTO_ORIENTATION:
+                       qui_utils.set_autorient(self.window, True)
+                       qui_utils.set_window_orientation(self.window, None)
+               elif orientation == self._app.LANDSCAPE_ORIENTATION:
+                       qui_utils.set_autorient(self.window, False)
+                       qui_utils.set_window_orientation(self.window, QtCore.Qt.Horizontal)
+               elif orientation == self._app.PORTRAIT_ORIENTATION:
+                       qui_utils.set_autorient(self.window, False)
                        qui_utils.set_window_orientation(self.window, QtCore.Qt.Vertical)
                else:
-                       qui_utils.set_window_orientation(self.window, QtCore.Qt.Horizontal)
+                       raise RuntimeError("Unknown orientation: %r" % orientation)
                for child in self.walk_children():
-                       child.set_orientation(isPortrait)
+                       child.update_orientation(orientation)
 
        @misc_utils.log_exception(_moduleLogger)
        def _on_child_close(self, obj = None):
index b0e205a..5d91fed 100755 (executable)
@@ -30,15 +30,28 @@ __email__ = "eopage@byu.net"
 __version__ = constants.__version__
 __build__ = constants.__build__
 __changelog__ = """
-* Couldn't update SMS conversations in SMS window, fixed
+* In-application alert system for new messages
+* Auto-update of voicemail on missed calls
+* Ability to only refresh SMS or Voicemail for faster refreshes
+* Ability to refresh only parts of call history for faster refreshes
+* Voicemail audio download and playback
+* Account refresh for when connection has expired
+* Improved look of refresh buttons (Maemo 4.1)
+* Improved rotation settings
+* Auto-scroll SMS window for the user when on-screen-keyboard pops up (Maemo 4.1)
+* Added support for QtMobility Contacts (Note: there seems to be a bug in libraries I depend on when run as root)
+* Improving the settings dialog
+* Fixing message ordering
+* Limited log size due to longer release cycles
+* Some minor optimizations
+* Reduced network timeout, connections while transition networks wouldn't immediately die but timeout which took too long
+* Fixed text encoding issue for CSV contacts
 """.strip()
 
 
 __postinstall__ = """#!/bin/sh -e
 
 gtk-update-icon-cache -f /usr/share/icons/hicolor
-rm -f ~/.%(name)s/%(name)s.log
-rm -f ~/.%(name)s/notifier.log
 """ % {"name": constants.__app_name__}
 
 __preremove__ = """#!/bin/sh -e
@@ -90,7 +103,7 @@ def build_package(distribution):
        p.depends += {
                "debian": ", python-qt4",
                "diablo": ", python2.5-qt4-core, python2.5-qt4-gui",
-               "fremantle": ", python2.5-qt4-core, python2.5-qt4-gui, python2.5-qt4-maemo5",
+               "fremantle": ", python-pyside.qtgui, python-pyside.qtcore, python-pyside.qtmaemo5, python-qtmobility.contacts",
        }[distribution]
        p.recommends = ", ".join([
        ])