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/
--- /dev/null
+#!/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()
--- /dev/null
+#!/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()
import ConfigParser
import logging
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
import dbus
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:
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):
@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)
import ConfigParser
import pprint
import logging
+import logging.handlers
import constants
from backends.gvoice import gvoice
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):
loggedIn = False
if not loggedIn:
- loggedIn = backend.is_authed()
+ loggedIn = backend.refresh_account_info() is not None
if not loggedIn:
import base64
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:
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:])
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
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(
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)
if len(row[phoneColumn]) == 0:
continue
contactDetails.append({
- "phoneType": phoneType,
+ "phoneType": try_unicode(phoneType),
"phoneNumber": row[phoneColumn],
})
except IndexError:
break
else:
fullName = "Unknown"
+ fullName = try_unicode(fullName)
yield str(yieldCount), {
"contactId": "%s-%d" % (self._name, yieldCount),
"name": fullName,
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):
"""
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):
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):
"""
"""
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
_moduleLogger = logging.getLogger(__name__)
-socket.setdefaulttimeout(45)
+socket.setdefaulttimeout(25)
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.
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"
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)
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)
"btmpl": "mobile",
"PersistentCookie": "yes",
"GALX": token,
- "continue": self._forwardURL,
+ "continue": self._JSON_CONTACTS_URL,
}
loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
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()
self._browser.save_cookies()
self._token = None
self._lastAuthed = 0.0
+ self._callbackNumbers = {}
def is_dnd(self):
"""
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):
"""
"""
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 = {
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)
browser = backend._browser
_TEST_WEBPAGES = [
- ("forward", backend._forwardURL),
("token", backend._tokenURL),
("login", backend._loginURL),
("isdnd", backend._isDndURL),
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
)
+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)
password = args[2]
grab_debug_info(username, password)
+ grab_voicemails(username, password)
if __name__ == "__main__":
def name(self):
return "None"
- def update_contacts(self, force = True):
+ def update_account(self, force = True):
pass
def get_contacts(self):
--- /dev/null
+#!/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()
--- /dev/null
+#!/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
+
__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__)
"""
-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()
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
_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 = [
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)
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:
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:
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):
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_)
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
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
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)
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)
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"
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()
)
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):
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):
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):
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):
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()
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)
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
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):
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
(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)
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()
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)
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):
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(
@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):
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):
)
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:
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
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()
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)
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:
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:
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
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)
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:
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):
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):
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):
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):
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):
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(), ))
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
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):
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):
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):
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()
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
)
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,
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):
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):
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 = []
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):
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,
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):
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):
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)
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 = []
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,
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):
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):
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):
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))
]
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):
import dbus
-class LedHandler(object):
+class _NokiaLedHandler(object):
def __init__(self):
self._bus = dbus.SystemBus()
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()
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
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
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 = ""
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):
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):
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()
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
# @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)
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):
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()
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):
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):
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:
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):
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
{},
)
- 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:
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:
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
try:
dataToDump = (
constants.__version__, constants.__build__,
- self._contacts, self._contactUpdateTime,
self._messages, self._messageUpdateTime,
self._history, self._historyUpdateTime,
self._dnd, self._callback
_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 = []
self._dnd = False
self._callback = ""
- if updateContacts:
- self.contactsUpdated.emit()
if updateMessages:
self.messagesUpdated.emit()
if updateHistory:
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:
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:
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)
_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
--- /dev/null
+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)
--- /dev/null
+#!/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
+
--- /dev/null
+#!/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
+
--- /dev/null
+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)
_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"
try:
trampoline, args, kwds = self._run.send(result)
except StopIteration, e:
- pass
+ self._isDone = True
else:
self._pool.add_task(
trampoline,
try:
trampoline, args, kwds = self._run.throw(error)
except StopIteration, e:
- pass
+ self._isDone = True
else:
self._pool.add_task(
trampoline,
_QUEUE_EMPTY = object()
-class AsyncPool(object):
+class FutureThread(object):
def __init__(self):
self.__workQueue = Queue.Queue()
import logging
-from PyQt4 import QtCore
+import qt_compat
+QtCore = qt_compat.QtCore
import misc
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
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")
--- /dev/null
+#!/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
+
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
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
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)
import os
import warnings
-from PyQt4 import QtGui
+import qt_compat
+QtGui = qt_compat.import_module("QtGui")
import qtpie
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
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)
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)
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)
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:
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):
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:
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:
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
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
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")
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):
return self._orientationAction
@property
+ def orientation(self):
+ return self._orientation
+
+ @property
def logAction(self):
return self._logAction
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()
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()
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):
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 ()
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):
__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
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([
])