From 94e4db64f31b1b1e06193a04bed1bb6e4dbff814 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 18 Aug 2010 20:36:20 -0500 Subject: [PATCH] Creating a hollow shell of a UI --- src/backends/browser_emu.py | 206 ---- src/backends/gv_backend.py | 4 +- src/backends/gvoice.py | 984 ------------------- src/backends/gvoice/browser_emu.py | 206 ++++ src/backends/gvoice/gvoice.py | 984 +++++++++++++++++++ src/backends/merge_backend.py | 2 +- src/dc_glade.py | 1006 -------------------- src/dialcentral.glade | 1406 --------------------------- src/dialcentral.py | 58 +- src/dialcentral_qt.py | 447 +++++++++ src/gtk_toolbox.py | 760 --------------- src/gv_views.py | 1827 ------------------------------------ src/hildonize.py | 766 --------------- src/maeqt.py | 122 +++ src/null_views.py | 251 ----- 15 files changed, 1798 insertions(+), 7231 deletions(-) delete mode 100644 src/backends/browser_emu.py delete mode 100755 src/backends/gvoice.py create mode 100644 src/backends/gvoice/__init__.py create mode 100644 src/backends/gvoice/browser_emu.py create mode 100755 src/backends/gvoice/gvoice.py delete mode 100755 src/dc_glade.py delete mode 100644 src/dialcentral.glade create mode 100755 src/dialcentral_qt.py delete mode 100644 src/gtk_toolbox.py delete mode 100644 src/gv_views.py delete mode 100644 src/hildonize.py create mode 100644 src/maeqt.py delete mode 100644 src/null_views.py diff --git a/src/backends/browser_emu.py b/src/backends/browser_emu.py deleted file mode 100644 index 5e9b678..0000000 --- a/src/backends/browser_emu.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -@author: Laszlo Nagy -@copyright: (c) 2005 by Szoftver Messias Bt. -@licence: BSD style - -Objects of the MozillaEmulator class can emulate a browser that is capable of: - - - cookie management - - configurable user agent string - - GET and POST - - multipart POST (send files) - - receive content into file - -I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it: - - 1. Use firefox - 2. Install and open the livehttpheaders plugin - 3. Use the website manually with firefox - 4. Check the GET and POST requests in the livehttpheaders capture window - 5. Create an instance of the above class and send the same GET and POST requests to the server. - -Optional steps: - - - You can change user agent string in the build_opened method - - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files -""" - -import urllib2 -import cookielib -import logging - -import socket - - -_moduleLogger = logging.getLogger(__name__) -socket.setdefaulttimeout(45) - - -def add_proxy(protocol, url, port): - proxyInfo = "%s:%s" % (url, port) - proxy = urllib2.ProxyHandler( - {protocol: proxyInfo} - ) - opener = urllib2.build_opener(proxy) - urllib2.install_opener(opener) - - -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)' - - def __init__(self, trycount = 1): - """Create a new MozillaEmulator object. - - @param trycount: The download() method will retry the operation if it - fails. You can specify -1 for infinite retrying. A value of 0 means no - retrying. A value of 1 means one retry. etc.""" - self.debug = False - self.trycount = trycount - self._cookies = cookielib.LWPCookieJar() - self._loadedFromCookies = False - - def load_cookies(self, path): - assert not self._loadedFromCookies, "Load cookies only once" - if path is None: - return - - self._cookies.filename = path - try: - self._cookies.load() - except cookielib.LoadError: - _moduleLogger.exception("Bad cookie file") - except IOError: - _moduleLogger.exception("No cookie file") - except Exception, e: - _moduleLogger.exception("Unknown error with cookies") - self._loadedFromCookies = True - - return self._loadedFromCookies - - def save_cookies(self): - if self._loadedFromCookies: - self._cookies.save() - - def clear_cookies(self): - if self._loadedFromCookies: - self._cookies.clear() - - def download(self, url, - postdata = None, extraheaders = None, forbidRedirect = False, - trycount = None, only_head = False, - ): - """Download an URL with GET or POST methods. - - @param postdata: It can be a string that will be POST-ed to the URL. - When None is given, the method will be GET instead. - @param extraheaders: You can add/modify HTTP headers with a dict here. - @param forbidRedirect: Set this flag if you do not want to handle - HTTP 301 and 302 redirects. - @param trycount: Specify the maximum number of retries here. - 0 means no retry on error. Using -1 means infinite retring. - None means the default value (that is self.trycount). - @param only_head: Create the openerdirector and return it. In other - words, this will not retrieve any content except HTTP headers. - - @return: The raw HTML page data - """ - _moduleLogger.debug("Performing download of %s" % url) - - if extraheaders is None: - extraheaders = {} - if trycount is None: - trycount = self.trycount - cnt = 0 - - while True: - try: - req, u = self._build_opener(url, postdata, extraheaders, forbidRedirect) - openerdirector = u.open(req) - if self.debug: - _moduleLogger.info("%r - %r" % (req.get_method(), url)) - _moduleLogger.info("%r - %r" % (openerdirector.code, openerdirector.msg)) - _moduleLogger.info("%r" % (openerdirector.headers)) - self._cookies.extract_cookies(openerdirector, req) - if only_head: - return openerdirector - - return self._read(openerdirector, trycount) - except urllib2.URLError, e: - _moduleLogger.debug("%s: %s" % (e, url)) - cnt += 1 - if (-1 < trycount) and (trycount < cnt): - raise - - # Retry :-) - _moduleLogger.debug("MozillaEmulator: urllib2.URLError, retrying %d" % cnt) - - def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False): - if extraheaders is None: - extraheaders = {} - - txheaders = { - 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png', - 'Accept-Language': 'en,en-us;q=0.5', - 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', - 'User-Agent': self.USER_AGENT, - } - for key, value in extraheaders.iteritems(): - txheaders[key] = value - req = urllib2.Request(url, postdata, txheaders) - self._cookies.add_cookie_header(req) - if forbidRedirect: - redirector = HTTPNoRedirector() - #_moduleLogger.info("Redirection disabled") - else: - redirector = urllib2.HTTPRedirectHandler() - #_moduleLogger.info("Redirection enabled") - - http_handler = urllib2.HTTPHandler(debuglevel=self.debug) - https_handler = urllib2.HTTPSHandler(debuglevel=self.debug) - - u = urllib2.build_opener( - http_handler, - https_handler, - urllib2.HTTPCookieProcessor(self._cookies), - redirector - ) - if not postdata is None: - req.add_data(postdata) - return (req, u) - - def _read(self, openerdirector, trycount): - chunks = [] - - chunk = openerdirector.read() - chunks.append(chunk) - #while chunk and cnt < trycount: - # time.sleep(1) - # cnt += 1 - # chunk = openerdirector.read() - # chunks.append(chunk) - - data = "".join(chunks) - - if "Content-Length" in openerdirector.info(): - assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % ( - openerdirector.info()["Content-Length"], - len(data), - ) - - return data - - -class HTTPNoRedirector(urllib2.HTTPRedirectHandler): - """This is a custom http redirect handler that FORBIDS redirection.""" - - def http_error_302(self, req, fp, code, msg, headers): - e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) - if e.code in (301, 302): - if 'location' in headers: - newurl = headers.getheaders('location')[0] - elif 'uri' in headers: - newurl = headers.getheaders('uri')[0] - e.newurl = newurl - _moduleLogger.info("New url: %s" % e.newurl) - raise e diff --git a/src/backends/gv_backend.py b/src/backends/gv_backend.py index ace5243..49294e7 100644 --- a/src/backends/gv_backend.py +++ b/src/backends/gv_backend.py @@ -30,10 +30,10 @@ from __future__ import with_statement import itertools import logging -import gvoice +from gvoice import gvoice -_moduleLogger = logging.getLogger("gv_backend") +_moduleLogger = logging.getLogger(__name__) class GVDialer(object): diff --git a/src/backends/gvoice.py b/src/backends/gvoice.py deleted file mode 100755 index 187c394..0000000 --- a/src/backends/gvoice.py +++ /dev/null @@ -1,984 +0,0 @@ -#!/usr/bin/python - -""" -DialCentral - Front end for Google's GoogleVoice service. -Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Google Voice backend code - -Resources - http://thatsmith.com/2009/03/google-voice-addon-for-firefox/ - http://posttopic.com/topic/google-voice-add-on-development -""" - -from __future__ import with_statement - -import os -import re -import urllib -import urllib2 -import time -import datetime -import itertools -import logging -import inspect - -from xml.sax import saxutils -from xml.etree import ElementTree - -try: - import simplejson as _simplejson - simplejson = _simplejson -except ImportError: - simplejson = None - -import browser_emu - - -_moduleLogger = logging.getLogger(__name__) - - -class NetworkError(RuntimeError): - pass - - -class MessageText(object): - - ACCURACY_LOW = "med1" - ACCURACY_MEDIUM = "med2" - ACCURACY_HIGH = "high" - - def __init__(self): - self.accuracy = None - self.text = None - - def __str__(self): - return self.text - - def to_dict(self): - return to_dict(self) - - def __eq__(self, other): - return self.accuracy == other.accuracy and self.text == other.text - - -class Message(object): - - def __init__(self): - self.whoFrom = None - self.body = None - self.when = None - - def __str__(self): - return "%s (%s): %s" % ( - self.whoFrom, - self.when, - "".join(str(part) for part in self.body) - ) - - def to_dict(self): - selfDict = to_dict(self) - selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None - return selfDict - - def __eq__(self, other): - return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body - - -class Conversation(object): - - TYPE_VOICEMAIL = "Voicemail" - TYPE_SMS = "SMS" - - def __init__(self): - self.type = None - self.id = None - self.contactId = None - self.name = None - self.location = None - self.prettyNumber = None - self.number = None - - self.time = None - self.relTime = None - self.messages = None - self.isRead = None - self.isSpam = None - self.isTrash = None - self.isArchived = None - - def __cmp__(self, other): - cmpValue = cmp(self.contactId, other.contactId) - if cmpValue != 0: - return cmpValue - - cmpValue = cmp(self.time, other.time) - if cmpValue != 0: - return cmpValue - - cmpValue = cmp(self.id, other.id) - if cmpValue != 0: - return cmpValue - - def to_dict(self): - selfDict = to_dict(self) - selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None - return selfDict - - -class GVoiceBackend(object): - """ - This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers - the functions include login, setting up a callback number, and initalting a callback - """ - - PHONE_TYPE_HOME = 1 - PHONE_TYPE_MOBILE = 2 - PHONE_TYPE_WORK = 3 - PHONE_TYPE_GIZMO = 7 - - def __init__(self, cookieFile = None): - # Important items in this function are the setup of the browser emulation and cookie file - self._browser = browser_emu.MozillaEmulator(1) - self._loadedFromCookies = self._browser.load_cookies(cookieFile) - - self._token = "" - self._accountNum = "" - self._lastAuthed = 0.0 - self._callbackNumber = "" - self._callbackNumbers = {} - - # Suprisingly, moving all of these from class to self sped up startup time - - self._validateRe = re.compile("^\+?[0-9]{10,}$") - - self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth" - - 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._sendSmsURL = SECURE_URL_BASE + "sms/send" - - self._isDndURL = "https://www.google.com/voice/m/donotdisturb" - self._isDndRe = re.compile(r"""""") - self._setDndURL = "https://www.google.com/voice/m/savednd" - - self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/" - self._markAsReadURL = SECURE_URL_BASE + "m/mark" - self._archiveMessageURL = SECURE_URL_BASE + "m/archive" - - self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/" - self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/" - # HACK really this redirects to the main pge and we are grabbing some javascript - self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact" - self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/" - - self.XML_FEEDS = ( - 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms', - 'recorded', 'placed', 'received', 'missed' - ) - self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox" - self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred" - self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all" - self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam" - self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash" - self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/" - self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/" - self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/" - self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/" - self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/" - self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/" - - self._galxRe = re.compile(r"""""", re.MULTILINE | re.DOTALL) - self._tokenRe = re.compile(r"""""") - self._accountNumRe = re.compile(r"""(.{14})""") - self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)\s*$""", re.M) - - self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL) - self._seperateVoicemailsRegex = re.compile(r"""^\s*
""", re.MULTILINE | re.DOTALL) - self._exactVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) - self._relativeVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) - self._voicemailNameRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - self._voicemailNumberRegex = re.compile(r"""""", re.MULTILINE) - self._prettyVoicemailNumberRegex = re.compile(r"""(.*?)""", re.MULTILINE) - self._voicemailLocationRegex = re.compile(r""".*?(.*?)""", re.MULTILINE) - self._messagesContactIDRegex = re.compile(r""".*?\s*?(.*?)""", re.MULTILINE) - self._voicemailMessageRegex = re.compile(r"""((.*?)|(.*?))""", re.MULTILINE) - self._smsFromRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - self._smsTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - self._smsTextRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - - def is_quick_login_possible(self): - """ - @returns True then is_authed 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 - """ - isRecentledAuthed = (time.time() - self._lastAuthed) < 120 - isPreviouslyAuthed = self._token is not None - if isRecentledAuthed and isPreviouslyAuthed and not force: - return True - - try: - page = self._get_page(self._forwardURL) - self._grab_account_info(page) - except Exception, e: - _moduleLogger.exception(str(e)) - return False - - self._browser.save_cookies() - self._lastAuthed = time.time() - return True - - def _get_token(self): - tokenPage = self._get_page(self._tokenURL) - - galxTokens = self._galxRe.search(tokenPage) - if galxTokens is not None: - galxToken = galxTokens.group(1) - else: - galxToken = "" - _moduleLogger.debug("Could not grab GALX token") - return galxToken - - def _login(self, username, password, token): - loginData = { - 'Email' : username, - 'Passwd' : password, - 'service': "grandcentral", - "ltmpl": "mobile", - "btmpl": "mobile", - "PersistentCookie": "yes", - "GALX": token, - "continue": self._forwardURL, - } - - loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData) - return loginSuccessOrFailurePage - - def login(self, username, password): - """ - Attempt to login to GoogleVoice - @returns Whether login was successful or not - """ - self.logout() - galxToken = self._get_token() - loginSuccessOrFailurePage = self._login(username, password, galxToken) - - try: - 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: - _moduleLogger.exception(str(e)) - return False - _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this") - - self._browser.save_cookies() - self._lastAuthed = time.time() - return True - - def logout(self): - self._browser.clear_cookies() - self._browser.save_cookies() - self._token = None - self._lastAuthed = 0.0 - - def is_dnd(self): - isDndPage = self._get_page(self._isDndURL) - - dndGroup = self._isDndRe.search(isDndPage) - if dndGroup is None: - return False - dndStatus = dndGroup.group(1) - isDnd = True if dndStatus.strip().lower() == "true" else False - return isDnd - - def set_dnd(self, doNotDisturb): - dndPostData = { - "doNotDisturb": 1 if doNotDisturb else 0, - } - - dndPage = self._get_page_with_token(self._setDndURL, dndPostData) - - def call(self, outgoingNumber): - """ - This is the main function responsible for initating the callback - """ - outgoingNumber = self._send_validation(outgoingNumber) - subscriberNumber = None - phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack - - callData = { - 'outgoingNumber': outgoingNumber, - 'forwardingNumber': self._callbackNumber, - 'subscriberNumber': subscriberNumber or 'undefined', - 'phoneType': str(phoneType), - 'remember': '1', - } - _moduleLogger.info("%r" % callData) - - page = self._get_page_with_token( - self._callUrl, - callData, - ) - self._parse_with_validation(page) - return True - - def cancel(self, outgoingNumber=None): - """ - Cancels a call matching outgoing and forwarding numbers (if given). - Will raise an error if no matching call is being placed - """ - page = self._get_page_with_token( - self._callCancelURL, - { - 'outgoingNumber': outgoingNumber or 'undefined', - 'forwardingNumber': self._callbackNumber or 'undefined', - 'cancelType': 'C2C', - }, - ) - self._parse_with_validation(page) - - def send_sms(self, phoneNumbers, message): - validatedPhoneNumbers = [ - self._send_validation(phoneNumber) - for phoneNumber in phoneNumbers - ] - flattenedPhoneNumbers = ",".join(validatedPhoneNumbers) - page = self._get_page_with_token( - self._sendSmsURL, - { - 'phoneNumber': flattenedPhoneNumbers, - 'text': message - }, - ) - self._parse_with_validation(page) - - def search(self, query): - """ - Search your Google Voice Account history for calls, voicemails, and sms - Returns ``Folder`` instance containting matching messages - """ - page = self._get_page( - self._XML_SEARCH_URL, - {"q": query}, - ) - json, html = extract_payload(page) - return json - - def get_feed(self, feed): - actualFeed = "_XML_%s_URL" % feed.upper() - feedUrl = getattr(self, actualFeed) - - page = self._get_page(feedUrl) - json, html = extract_payload(page) - - return json - - def download(self, messageId, adir): - """ - 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. - """ - page = self._get_page(self._downloadVoicemailURL, {"id": messageId}) - fn = os.path.join(adir, '%s.mp3' % messageId) - with open(fn, 'wb') as fo: - fo.write(page) - return fn - - def is_valid_syntax(self, number): - """ - @returns If This number be called ( syntax validation only ) - """ - return self._validateRe.match(number) is not None - - def get_account_number(self): - """ - @returns The GoogleVoice phone number - """ - return self._accountNum - - def get_callback_numbers(self): - """ - @returns a dictionary mapping call back numbers to descriptions - @note These results are cached for 30 minutes. - """ - if not self.is_authed(): - return {} - return self._callbackNumbers - - def set_callback_number(self, callbacknumber): - """ - Set the number that GoogleVoice calls - @param callbacknumber should be a proper 10 digit number - """ - self._callbackNumber = callbacknumber - _moduleLogger.info("Callback number changed: %r" % self._callbackNumber) - return True - - def get_callback_number(self): - """ - @returns Current callback number or None - """ - return self._callbackNumber - - def get_recent(self): - """ - @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) - """ - for action, url in ( - ("Received", self._XML_RECEIVED_URL), - ("Missed", self._XML_MISSED_URL), - ("Placed", self._XML_PLACED_URL), - ): - flatXml = self._get_page(url) - - allRecentHtml = self._grab_html(flatXml) - allRecentData = self._parse_history(allRecentHtml) - for recentCallData in allRecentData: - recentCallData["action"] = action - yield recentCallData - - def get_contacts(self): - """ - @returns Iterable of (contact id, contact name) - """ - page = self._get_page(self._XML_CONTACTS_URL) - contactsBody = self._contactsBodyRe.search(page) - if contactsBody is None: - raise RuntimeError("Could not extract contact information") - accountData = _fake_parse_json(contactsBody.group(1)) - for contactId, contactDetails in accountData["contacts"].iteritems(): - # A zero contact id is the catch all for unknown contacts - if contactId != "0": - if "name" in contactDetails: - contactDetails["name"] = unescape(contactDetails["name"]) - yield contactId, contactDetails - - def get_voicemails(self): - voicemailPage = self._get_page(self._XML_VOICEMAIL_URL) - voicemailHtml = self._grab_html(voicemailPage) - voicemailJson = self._grab_json(voicemailPage) - parsedVoicemail = self._parse_voicemail(voicemailHtml) - voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson) - return voicemails - - def get_texts(self): - smsPage = self._get_page(self._XML_SMS_URL) - smsHtml = self._grab_html(smsPage) - smsJson = self._grab_json(smsPage) - parsedSms = self._parse_sms(smsHtml) - smss = self._merge_conversation_sources(parsedSms, smsJson) - return smss - - def mark_message(self, messageId, asRead): - postData = { - "read": 1 if asRead else 0, - "id": messageId, - } - - markPage = self._get_page(self._markAsReadURL, postData) - - def archive_message(self, messageId): - postData = { - "id": messageId, - } - - markPage = self._get_page(self._archiveMessageURL, postData) - - def _grab_json(self, flatXml): - xmlTree = ElementTree.fromstring(flatXml) - jsonElement = xmlTree.getchildren()[0] - flatJson = jsonElement.text - jsonTree = parse_json(flatJson) - return jsonTree - - def _grab_html(self, flatXml): - xmlTree = ElementTree.fromstring(flatXml) - htmlElement = xmlTree.getchildren()[1] - flatHtml = htmlElement.text - 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 - if len(self._callbackNumbers) == 0: - _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page) - - 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_history(self, historyHtml): - splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml) - for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): - exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) - exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" - exactTime = google_strptime(exactTime) - relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) - relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" - locationGroup = self._voicemailLocationRegex.search(messageHtml) - location = locationGroup.group(1).strip() if locationGroup else "" - - nameGroup = self._voicemailNameRegex.search(messageHtml) - name = nameGroup.group(1).strip() if nameGroup else "" - numberGroup = self._voicemailNumberRegex.search(messageHtml) - number = numberGroup.group(1).strip() if numberGroup else "" - prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) - prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" - contactIdGroup = self._messagesContactIDRegex.search(messageHtml) - contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" - - yield { - "id": messageId.strip(), - "contactId": contactId, - "name": unescape(name), - "time": exactTime, - "relTime": relativeTime, - "prettyNumber": prettyNumber, - "number": number, - "location": unescape(location), - } - - @staticmethod - def _interpret_voicemail_regex(group): - quality, content, number = group.group(2), group.group(3), group.group(4) - text = MessageText() - if quality is not None and content is not None: - text.accuracy = quality - text.text = unescape(content) - return text - elif number is not None: - text.accuracy = MessageText.ACCURACY_HIGH - text.text = number - return text - - def _parse_voicemail(self, voicemailHtml): - splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml) - for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): - conv = Conversation() - conv.type = Conversation.TYPE_VOICEMAIL - conv.id = messageId.strip() - - exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) - exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else "" - conv.time = google_strptime(exactTimeText) - relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) - conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" - locationGroup = self._voicemailLocationRegex.search(messageHtml) - conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "") - - nameGroup = self._voicemailNameRegex.search(messageHtml) - conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "") - numberGroup = self._voicemailNumberRegex.search(messageHtml) - conv.number = numberGroup.group(1).strip() if numberGroup else "" - prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) - conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" - contactIdGroup = self._messagesContactIDRegex.search(messageHtml) - conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" - - messageGroups = self._voicemailMessageRegex.finditer(messageHtml) - messageParts = [ - self._interpret_voicemail_regex(group) - for group in messageGroups - ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), ) - message = Message() - message.body = messageParts - message.whoFrom = conv.name - message.when = conv.time.strftime("%I:%M %p") - conv.messages = (message, ) - - yield conv - - @staticmethod - def _interpret_sms_message_parts(fromPart, textPart, timePart): - text = MessageText() - text.accuracy = MessageText.ACCURACY_MEDIUM - text.text = unescape(textPart) - - message = Message() - message.body = (text, ) - message.whoFrom = fromPart - message.when = timePart - - return message - - def _parse_sms(self, smsHtml): - splitSms = self._seperateVoicemailsRegex.split(smsHtml) - for messageId, messageHtml in itergroup(splitSms[1:], 2): - conv = Conversation() - conv.type = Conversation.TYPE_SMS - conv.id = messageId.strip() - - exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) - exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else "" - conv.time = google_strptime(exactTimeText) - relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) - conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" - conv.location = "" - - nameGroup = self._voicemailNameRegex.search(messageHtml) - conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "") - numberGroup = self._voicemailNumberRegex.search(messageHtml) - conv.number = numberGroup.group(1).strip() if numberGroup else "" - prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) - conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" - contactIdGroup = self._messagesContactIDRegex.search(messageHtml) - conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" - - fromGroups = self._smsFromRegex.finditer(messageHtml) - fromParts = (group.group(1).strip() for group in fromGroups) - textGroups = self._smsTextRegex.finditer(messageHtml) - textParts = (group.group(1).strip() for group in textGroups) - timeGroups = self._smsTimeRegex.finditer(messageHtml) - timeParts = (group.group(1).strip() for group in timeGroups) - - messageParts = itertools.izip(fromParts, textParts, timeParts) - messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts] - conv.messages = messages - - yield conv - - @staticmethod - def _merge_conversation_sources(parsedMessages, json): - for message in parsedMessages: - jsonItem = json["messages"][message.id] - message.isRead = jsonItem["isRead"] - message.isSpam = jsonItem["isSpam"] - message.isTrash = jsonItem["isTrash"] - message.isArchived = "inbox" not in jsonItem["labels"] - yield message - - def _get_page(self, url, data = None, refererUrl = None): - headers = {} - if refererUrl is not None: - headers["Referer"] = refererUrl - - encodedData = urllib.urlencode(data) if data is not None else None - - try: - page = self._browser.download(url, encodedData, None, headers) - except urllib2.URLError, e: - _moduleLogger.error("Translating error: %s" % str(e)) - raise NetworkError("%s is not accesible" % url) - - return page - - def _get_page_with_token(self, url, data = None, refererUrl = None): - if data is None: - data = {} - data['_rnr_se'] = self._token - - page = self._get_page(url, data, refererUrl) - - return page - - def _parse_with_validation(self, page): - json = parse_json(page) - validate_response(json) - return json - - -_UNESCAPE_ENTITIES = { - """: '"', - " ": " ", - "'": "'", -} - - -def unescape(text): - plain = saxutils.unescape(text, _UNESCAPE_ENTITIES) - return plain - - -def google_strptime(time): - """ - Hack: Google always returns the time in the same locale. Sadly if the - local system's locale is different, there isn't a way to perfectly handle - the time. So instead we handle implement some time formatting - """ - abbrevTime = time[:-3] - parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M") - if time[-2] == "PN": - parsedTime += datetime.timedelta(hours=12) - return parsedTime - - -def itergroup(iterator, count, padValue = None): - """ - Iterate in groups of 'count' values. If there - aren't enough values, the last result is padded with - None. - - >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): - ... print tuple(val) - (1, 2, 3) - (4, 5, 6) - >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): - ... print list(val) - [1, 2, 3] - [4, 5, 6] - >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3): - ... print tuple(val) - (1, 2, 3) - (4, 5, 6) - (7, None, None) - >>> for val in itergroup("123456", 3): - ... print tuple(val) - ('1', '2', '3') - ('4', '5', '6') - >>> for val in itergroup("123456", 3): - ... print repr("".join(val)) - '123' - '456' - """ - paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1)) - nIterators = (paddedIterator, ) * count - return itertools.izip(*nIterators) - - -def safe_eval(s): - _TRUE_REGEX = re.compile("true") - _FALSE_REGEX = re.compile("false") - s = _TRUE_REGEX.sub("True", s) - s = _FALSE_REGEX.sub("False", s) - return eval(s, {}, {}) - - -def _fake_parse_json(flattened): - return safe_eval(flattened) - - -def _actual_parse_json(flattened): - return simplejson.loads(flattened) - - -if simplejson is None: - parse_json = _fake_parse_json -else: - parse_json = _actual_parse_json - - -def extract_payload(flatXml): - xmlTree = ElementTree.fromstring(flatXml) - - jsonElement = xmlTree.getchildren()[0] - flatJson = jsonElement.text - jsonTree = parse_json(flatJson) - - htmlElement = xmlTree.getchildren()[1] - flatHtml = htmlElement.text - - return jsonTree, flatHtml - - -def validate_response(response): - """ - Validates that the JSON response is A-OK - """ - try: - assert 'ok' in response and response['ok'] - except AssertionError: - raise RuntimeError('There was a problem with GV: %s' % response) - - -def guess_phone_type(number): - if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"): - return GVoiceBackend.PHONE_TYPE_GIZMO - else: - return GVoiceBackend.PHONE_TYPE_MOBILE - - -def get_sane_callback(backend): - """ - Try to set a sane default callback number on these preferences - 1) 1747 numbers ( Gizmo ) - 2) anything with gizmo in the name - 3) anything with computer in the name - 4) the first value - """ - numbers = backend.get_callback_numbers() - - priorityOrderedCriteria = [ - ("\+1747", None), - ("1747", None), - ("747", None), - (None, "gizmo"), - (None, "computer"), - (None, "sip"), - (None, None), - ] - - for numberCriteria, descriptionCriteria in priorityOrderedCriteria: - numberMatcher = None - descriptionMatcher = None - if numberCriteria is not None: - numberMatcher = re.compile(numberCriteria) - elif descriptionCriteria is not None: - descriptionMatcher = re.compile(descriptionCriteria, re.I) - - for number, description in numbers.iteritems(): - if numberMatcher is not None and numberMatcher.match(number) is None: - continue - if descriptionMatcher is not None and descriptionMatcher.match(description) is None: - continue - return number - - -def set_sane_callback(backend): - """ - Try to set a sane default callback number on these preferences - 1) 1747 numbers ( Gizmo ) - 2) anything with gizmo in the name - 3) anything with computer in the name - 4) the first value - """ - number = get_sane_callback(backend) - backend.set_callback_number(number) - - -def _is_not_special(name): - return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name - - -def to_dict(obj): - members = inspect.getmembers(obj) - return dict((name, value) for (name, value) in members if _is_not_special(name)) - - -def grab_debug_info(username, password): - cookieFile = os.path.join(".", "raw_cookies.txt") - try: - os.remove(cookieFile) - except OSError: - pass - - backend = GVoiceBackend(cookieFile) - browser = backend._browser - - _TEST_WEBPAGES = [ - ("forward", backend._forwardURL), - ("token", backend._tokenURL), - ("login", backend._loginURL), - ("isdnd", backend._isDndURL), - ("account", backend._XML_ACCOUNT_URL), - ("contacts", backend._XML_CONTACTS_URL), - - ("voicemail", backend._XML_VOICEMAIL_URL), - ("sms", backend._XML_SMS_URL), - - ("recent", backend._XML_RECENT_URL), - ("placed", backend._XML_PLACED_URL), - ("recieved", backend._XML_RECEIVED_URL), - ("missed", backend._XML_MISSED_URL), - ] - - # Get Pages - print "Grabbing pre-login pages" - for name, url in _TEST_WEBPAGES: - try: - page = browser.download(url) - except StandardError, e: - print e.message - continue - print "\tWriting to file" - with open("not_loggedin_%s.txt" % name, "w") as f: - f.write(page) - - # Login - print "Attempting login" - galxToken = backend._get_token() - loginSuccessOrFailurePage = backend._login(username, password, galxToken) - with open("loggingin.txt", "w") as f: - print "\tWriting to file" - f.write(loginSuccessOrFailurePage) - try: - 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) - if not loggedIn: - raise - - # Get Pages - print "Grabbing post-login pages" - for name, url in _TEST_WEBPAGES: - try: - page = browser.download(url) - except StandardError, e: - print str(e) - continue - print "\tWriting to file" - with open("loggedin_%s.txt" % name, "w") as f: - f.write(page) - - # Cookies - browser.save_cookies() - print "\tWriting cookies to file" - with open("cookies.txt", "w") as f: - f.writelines( - "%s: %s\n" % (c.name, c.value) - for c in browser._cookies - ) - - -def main(): - import sys - logging.basicConfig(level=logging.DEBUG) - args = sys.argv - if 3 <= len(args): - username = args[1] - password = args[2] - - grab_debug_info(username, password) - - -if __name__ == "__main__": - main() diff --git a/src/backends/gvoice/__init__.py b/src/backends/gvoice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backends/gvoice/browser_emu.py b/src/backends/gvoice/browser_emu.py new file mode 100644 index 0000000..5e9b678 --- /dev/null +++ b/src/backends/gvoice/browser_emu.py @@ -0,0 +1,206 @@ +""" +@author: Laszlo Nagy +@copyright: (c) 2005 by Szoftver Messias Bt. +@licence: BSD style + +Objects of the MozillaEmulator class can emulate a browser that is capable of: + + - cookie management + - configurable user agent string + - GET and POST + - multipart POST (send files) + - receive content into file + +I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it: + + 1. Use firefox + 2. Install and open the livehttpheaders plugin + 3. Use the website manually with firefox + 4. Check the GET and POST requests in the livehttpheaders capture window + 5. Create an instance of the above class and send the same GET and POST requests to the server. + +Optional steps: + + - You can change user agent string in the build_opened method + - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files +""" + +import urllib2 +import cookielib +import logging + +import socket + + +_moduleLogger = logging.getLogger(__name__) +socket.setdefaulttimeout(45) + + +def add_proxy(protocol, url, port): + proxyInfo = "%s:%s" % (url, port) + proxy = urllib2.ProxyHandler( + {protocol: proxyInfo} + ) + opener = urllib2.build_opener(proxy) + urllib2.install_opener(opener) + + +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)' + + def __init__(self, trycount = 1): + """Create a new MozillaEmulator object. + + @param trycount: The download() method will retry the operation if it + fails. You can specify -1 for infinite retrying. A value of 0 means no + retrying. A value of 1 means one retry. etc.""" + self.debug = False + self.trycount = trycount + self._cookies = cookielib.LWPCookieJar() + self._loadedFromCookies = False + + def load_cookies(self, path): + assert not self._loadedFromCookies, "Load cookies only once" + if path is None: + return + + self._cookies.filename = path + try: + self._cookies.load() + except cookielib.LoadError: + _moduleLogger.exception("Bad cookie file") + except IOError: + _moduleLogger.exception("No cookie file") + except Exception, e: + _moduleLogger.exception("Unknown error with cookies") + self._loadedFromCookies = True + + return self._loadedFromCookies + + def save_cookies(self): + if self._loadedFromCookies: + self._cookies.save() + + def clear_cookies(self): + if self._loadedFromCookies: + self._cookies.clear() + + def download(self, url, + postdata = None, extraheaders = None, forbidRedirect = False, + trycount = None, only_head = False, + ): + """Download an URL with GET or POST methods. + + @param postdata: It can be a string that will be POST-ed to the URL. + When None is given, the method will be GET instead. + @param extraheaders: You can add/modify HTTP headers with a dict here. + @param forbidRedirect: Set this flag if you do not want to handle + HTTP 301 and 302 redirects. + @param trycount: Specify the maximum number of retries here. + 0 means no retry on error. Using -1 means infinite retring. + None means the default value (that is self.trycount). + @param only_head: Create the openerdirector and return it. In other + words, this will not retrieve any content except HTTP headers. + + @return: The raw HTML page data + """ + _moduleLogger.debug("Performing download of %s" % url) + + if extraheaders is None: + extraheaders = {} + if trycount is None: + trycount = self.trycount + cnt = 0 + + while True: + try: + req, u = self._build_opener(url, postdata, extraheaders, forbidRedirect) + openerdirector = u.open(req) + if self.debug: + _moduleLogger.info("%r - %r" % (req.get_method(), url)) + _moduleLogger.info("%r - %r" % (openerdirector.code, openerdirector.msg)) + _moduleLogger.info("%r" % (openerdirector.headers)) + self._cookies.extract_cookies(openerdirector, req) + if only_head: + return openerdirector + + return self._read(openerdirector, trycount) + except urllib2.URLError, e: + _moduleLogger.debug("%s: %s" % (e, url)) + cnt += 1 + if (-1 < trycount) and (trycount < cnt): + raise + + # Retry :-) + _moduleLogger.debug("MozillaEmulator: urllib2.URLError, retrying %d" % cnt) + + def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False): + if extraheaders is None: + extraheaders = {} + + txheaders = { + 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png', + 'Accept-Language': 'en,en-us;q=0.5', + 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + 'User-Agent': self.USER_AGENT, + } + for key, value in extraheaders.iteritems(): + txheaders[key] = value + req = urllib2.Request(url, postdata, txheaders) + self._cookies.add_cookie_header(req) + if forbidRedirect: + redirector = HTTPNoRedirector() + #_moduleLogger.info("Redirection disabled") + else: + redirector = urllib2.HTTPRedirectHandler() + #_moduleLogger.info("Redirection enabled") + + http_handler = urllib2.HTTPHandler(debuglevel=self.debug) + https_handler = urllib2.HTTPSHandler(debuglevel=self.debug) + + u = urllib2.build_opener( + http_handler, + https_handler, + urllib2.HTTPCookieProcessor(self._cookies), + redirector + ) + if not postdata is None: + req.add_data(postdata) + return (req, u) + + def _read(self, openerdirector, trycount): + chunks = [] + + chunk = openerdirector.read() + chunks.append(chunk) + #while chunk and cnt < trycount: + # time.sleep(1) + # cnt += 1 + # chunk = openerdirector.read() + # chunks.append(chunk) + + data = "".join(chunks) + + if "Content-Length" in openerdirector.info(): + assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % ( + openerdirector.info()["Content-Length"], + len(data), + ) + + return data + + +class HTTPNoRedirector(urllib2.HTTPRedirectHandler): + """This is a custom http redirect handler that FORBIDS redirection.""" + + def http_error_302(self, req, fp, code, msg, headers): + e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) + if e.code in (301, 302): + if 'location' in headers: + newurl = headers.getheaders('location')[0] + elif 'uri' in headers: + newurl = headers.getheaders('uri')[0] + e.newurl = newurl + _moduleLogger.info("New url: %s" % e.newurl) + raise e diff --git a/src/backends/gvoice/gvoice.py b/src/backends/gvoice/gvoice.py new file mode 100755 index 0000000..187c394 --- /dev/null +++ b/src/backends/gvoice/gvoice.py @@ -0,0 +1,984 @@ +#!/usr/bin/python + +""" +DialCentral - Front end for Google's GoogleVoice service. +Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Google Voice backend code + +Resources + http://thatsmith.com/2009/03/google-voice-addon-for-firefox/ + http://posttopic.com/topic/google-voice-add-on-development +""" + +from __future__ import with_statement + +import os +import re +import urllib +import urllib2 +import time +import datetime +import itertools +import logging +import inspect + +from xml.sax import saxutils +from xml.etree import ElementTree + +try: + import simplejson as _simplejson + simplejson = _simplejson +except ImportError: + simplejson = None + +import browser_emu + + +_moduleLogger = logging.getLogger(__name__) + + +class NetworkError(RuntimeError): + pass + + +class MessageText(object): + + ACCURACY_LOW = "med1" + ACCURACY_MEDIUM = "med2" + ACCURACY_HIGH = "high" + + def __init__(self): + self.accuracy = None + self.text = None + + def __str__(self): + return self.text + + def to_dict(self): + return to_dict(self) + + def __eq__(self, other): + return self.accuracy == other.accuracy and self.text == other.text + + +class Message(object): + + def __init__(self): + self.whoFrom = None + self.body = None + self.when = None + + def __str__(self): + return "%s (%s): %s" % ( + self.whoFrom, + self.when, + "".join(str(part) for part in self.body) + ) + + def to_dict(self): + selfDict = to_dict(self) + selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None + return selfDict + + def __eq__(self, other): + return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body + + +class Conversation(object): + + TYPE_VOICEMAIL = "Voicemail" + TYPE_SMS = "SMS" + + def __init__(self): + self.type = None + self.id = None + self.contactId = None + self.name = None + self.location = None + self.prettyNumber = None + self.number = None + + self.time = None + self.relTime = None + self.messages = None + self.isRead = None + self.isSpam = None + self.isTrash = None + self.isArchived = None + + def __cmp__(self, other): + cmpValue = cmp(self.contactId, other.contactId) + if cmpValue != 0: + return cmpValue + + cmpValue = cmp(self.time, other.time) + if cmpValue != 0: + return cmpValue + + cmpValue = cmp(self.id, other.id) + if cmpValue != 0: + return cmpValue + + def to_dict(self): + selfDict = to_dict(self) + selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None + return selfDict + + +class GVoiceBackend(object): + """ + This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers + the functions include login, setting up a callback number, and initalting a callback + """ + + PHONE_TYPE_HOME = 1 + PHONE_TYPE_MOBILE = 2 + PHONE_TYPE_WORK = 3 + PHONE_TYPE_GIZMO = 7 + + def __init__(self, cookieFile = None): + # Important items in this function are the setup of the browser emulation and cookie file + self._browser = browser_emu.MozillaEmulator(1) + self._loadedFromCookies = self._browser.load_cookies(cookieFile) + + self._token = "" + self._accountNum = "" + self._lastAuthed = 0.0 + self._callbackNumber = "" + self._callbackNumbers = {} + + # Suprisingly, moving all of these from class to self sped up startup time + + self._validateRe = re.compile("^\+?[0-9]{10,}$") + + self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth" + + 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._sendSmsURL = SECURE_URL_BASE + "sms/send" + + self._isDndURL = "https://www.google.com/voice/m/donotdisturb" + self._isDndRe = re.compile(r"""""") + self._setDndURL = "https://www.google.com/voice/m/savednd" + + self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/" + self._markAsReadURL = SECURE_URL_BASE + "m/mark" + self._archiveMessageURL = SECURE_URL_BASE + "m/archive" + + self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/" + self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/" + # HACK really this redirects to the main pge and we are grabbing some javascript + self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact" + self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/" + + self.XML_FEEDS = ( + 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms', + 'recorded', 'placed', 'received', 'missed' + ) + self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox" + self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred" + self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all" + self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam" + self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash" + self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/" + self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/" + self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/" + self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/" + self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/" + self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/" + + self._galxRe = re.compile(r"""""", re.MULTILINE | re.DOTALL) + self._tokenRe = re.compile(r"""""") + self._accountNumRe = re.compile(r"""(.{14})
""") + self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)\s*$""", re.M) + + self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL) + self._seperateVoicemailsRegex = re.compile(r"""^\s*
""", re.MULTILINE | re.DOTALL) + self._exactVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._relativeVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._voicemailNameRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._voicemailNumberRegex = re.compile(r"""""", re.MULTILINE) + self._prettyVoicemailNumberRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._voicemailLocationRegex = re.compile(r""".*?(.*?)""", re.MULTILINE) + self._messagesContactIDRegex = re.compile(r""".*?\s*?(.*?)""", re.MULTILINE) + self._voicemailMessageRegex = re.compile(r"""((.*?)|(.*?))""", re.MULTILINE) + self._smsFromRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._smsTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._smsTextRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + + def is_quick_login_possible(self): + """ + @returns True then is_authed 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 + """ + isRecentledAuthed = (time.time() - self._lastAuthed) < 120 + isPreviouslyAuthed = self._token is not None + if isRecentledAuthed and isPreviouslyAuthed and not force: + return True + + try: + page = self._get_page(self._forwardURL) + self._grab_account_info(page) + except Exception, e: + _moduleLogger.exception(str(e)) + return False + + self._browser.save_cookies() + self._lastAuthed = time.time() + return True + + def _get_token(self): + tokenPage = self._get_page(self._tokenURL) + + galxTokens = self._galxRe.search(tokenPage) + if galxTokens is not None: + galxToken = galxTokens.group(1) + else: + galxToken = "" + _moduleLogger.debug("Could not grab GALX token") + return galxToken + + def _login(self, username, password, token): + loginData = { + 'Email' : username, + 'Passwd' : password, + 'service': "grandcentral", + "ltmpl": "mobile", + "btmpl": "mobile", + "PersistentCookie": "yes", + "GALX": token, + "continue": self._forwardURL, + } + + loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData) + return loginSuccessOrFailurePage + + def login(self, username, password): + """ + Attempt to login to GoogleVoice + @returns Whether login was successful or not + """ + self.logout() + galxToken = self._get_token() + loginSuccessOrFailurePage = self._login(username, password, galxToken) + + try: + 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: + _moduleLogger.exception(str(e)) + return False + _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this") + + self._browser.save_cookies() + self._lastAuthed = time.time() + return True + + def logout(self): + self._browser.clear_cookies() + self._browser.save_cookies() + self._token = None + self._lastAuthed = 0.0 + + def is_dnd(self): + isDndPage = self._get_page(self._isDndURL) + + dndGroup = self._isDndRe.search(isDndPage) + if dndGroup is None: + return False + dndStatus = dndGroup.group(1) + isDnd = True if dndStatus.strip().lower() == "true" else False + return isDnd + + def set_dnd(self, doNotDisturb): + dndPostData = { + "doNotDisturb": 1 if doNotDisturb else 0, + } + + dndPage = self._get_page_with_token(self._setDndURL, dndPostData) + + def call(self, outgoingNumber): + """ + This is the main function responsible for initating the callback + """ + outgoingNumber = self._send_validation(outgoingNumber) + subscriberNumber = None + phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack + + callData = { + 'outgoingNumber': outgoingNumber, + 'forwardingNumber': self._callbackNumber, + 'subscriberNumber': subscriberNumber or 'undefined', + 'phoneType': str(phoneType), + 'remember': '1', + } + _moduleLogger.info("%r" % callData) + + page = self._get_page_with_token( + self._callUrl, + callData, + ) + self._parse_with_validation(page) + return True + + def cancel(self, outgoingNumber=None): + """ + Cancels a call matching outgoing and forwarding numbers (if given). + Will raise an error if no matching call is being placed + """ + page = self._get_page_with_token( + self._callCancelURL, + { + 'outgoingNumber': outgoingNumber or 'undefined', + 'forwardingNumber': self._callbackNumber or 'undefined', + 'cancelType': 'C2C', + }, + ) + self._parse_with_validation(page) + + def send_sms(self, phoneNumbers, message): + validatedPhoneNumbers = [ + self._send_validation(phoneNumber) + for phoneNumber in phoneNumbers + ] + flattenedPhoneNumbers = ",".join(validatedPhoneNumbers) + page = self._get_page_with_token( + self._sendSmsURL, + { + 'phoneNumber': flattenedPhoneNumbers, + 'text': message + }, + ) + self._parse_with_validation(page) + + def search(self, query): + """ + Search your Google Voice Account history for calls, voicemails, and sms + Returns ``Folder`` instance containting matching messages + """ + page = self._get_page( + self._XML_SEARCH_URL, + {"q": query}, + ) + json, html = extract_payload(page) + return json + + def get_feed(self, feed): + actualFeed = "_XML_%s_URL" % feed.upper() + feedUrl = getattr(self, actualFeed) + + page = self._get_page(feedUrl) + json, html = extract_payload(page) + + return json + + def download(self, messageId, adir): + """ + 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. + """ + page = self._get_page(self._downloadVoicemailURL, {"id": messageId}) + fn = os.path.join(adir, '%s.mp3' % messageId) + with open(fn, 'wb') as fo: + fo.write(page) + return fn + + def is_valid_syntax(self, number): + """ + @returns If This number be called ( syntax validation only ) + """ + return self._validateRe.match(number) is not None + + def get_account_number(self): + """ + @returns The GoogleVoice phone number + """ + return self._accountNum + + def get_callback_numbers(self): + """ + @returns a dictionary mapping call back numbers to descriptions + @note These results are cached for 30 minutes. + """ + if not self.is_authed(): + return {} + return self._callbackNumbers + + def set_callback_number(self, callbacknumber): + """ + Set the number that GoogleVoice calls + @param callbacknumber should be a proper 10 digit number + """ + self._callbackNumber = callbacknumber + _moduleLogger.info("Callback number changed: %r" % self._callbackNumber) + return True + + def get_callback_number(self): + """ + @returns Current callback number or None + """ + return self._callbackNumber + + def get_recent(self): + """ + @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) + """ + for action, url in ( + ("Received", self._XML_RECEIVED_URL), + ("Missed", self._XML_MISSED_URL), + ("Placed", self._XML_PLACED_URL), + ): + flatXml = self._get_page(url) + + allRecentHtml = self._grab_html(flatXml) + allRecentData = self._parse_history(allRecentHtml) + for recentCallData in allRecentData: + recentCallData["action"] = action + yield recentCallData + + def get_contacts(self): + """ + @returns Iterable of (contact id, contact name) + """ + page = self._get_page(self._XML_CONTACTS_URL) + contactsBody = self._contactsBodyRe.search(page) + if contactsBody is None: + raise RuntimeError("Could not extract contact information") + accountData = _fake_parse_json(contactsBody.group(1)) + for contactId, contactDetails in accountData["contacts"].iteritems(): + # A zero contact id is the catch all for unknown contacts + if contactId != "0": + if "name" in contactDetails: + contactDetails["name"] = unescape(contactDetails["name"]) + yield contactId, contactDetails + + def get_voicemails(self): + voicemailPage = self._get_page(self._XML_VOICEMAIL_URL) + voicemailHtml = self._grab_html(voicemailPage) + voicemailJson = self._grab_json(voicemailPage) + parsedVoicemail = self._parse_voicemail(voicemailHtml) + voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson) + return voicemails + + def get_texts(self): + smsPage = self._get_page(self._XML_SMS_URL) + smsHtml = self._grab_html(smsPage) + smsJson = self._grab_json(smsPage) + parsedSms = self._parse_sms(smsHtml) + smss = self._merge_conversation_sources(parsedSms, smsJson) + return smss + + def mark_message(self, messageId, asRead): + postData = { + "read": 1 if asRead else 0, + "id": messageId, + } + + markPage = self._get_page(self._markAsReadURL, postData) + + def archive_message(self, messageId): + postData = { + "id": messageId, + } + + markPage = self._get_page(self._archiveMessageURL, postData) + + def _grab_json(self, flatXml): + xmlTree = ElementTree.fromstring(flatXml) + jsonElement = xmlTree.getchildren()[0] + flatJson = jsonElement.text + jsonTree = parse_json(flatJson) + return jsonTree + + def _grab_html(self, flatXml): + xmlTree = ElementTree.fromstring(flatXml) + htmlElement = xmlTree.getchildren()[1] + flatHtml = htmlElement.text + 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 + if len(self._callbackNumbers) == 0: + _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page) + + 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_history(self, historyHtml): + splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml) + for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + exactTime = google_strptime(exactTime) + relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) + relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" + locationGroup = self._voicemailLocationRegex.search(messageHtml) + location = locationGroup.group(1).strip() if locationGroup else "" + + nameGroup = self._voicemailNameRegex.search(messageHtml) + name = nameGroup.group(1).strip() if nameGroup else "" + numberGroup = self._voicemailNumberRegex.search(messageHtml) + number = numberGroup.group(1).strip() if numberGroup else "" + prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) + prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" + contactIdGroup = self._messagesContactIDRegex.search(messageHtml) + contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" + + yield { + "id": messageId.strip(), + "contactId": contactId, + "name": unescape(name), + "time": exactTime, + "relTime": relativeTime, + "prettyNumber": prettyNumber, + "number": number, + "location": unescape(location), + } + + @staticmethod + def _interpret_voicemail_regex(group): + quality, content, number = group.group(2), group.group(3), group.group(4) + text = MessageText() + if quality is not None and content is not None: + text.accuracy = quality + text.text = unescape(content) + return text + elif number is not None: + text.accuracy = MessageText.ACCURACY_HIGH + text.text = number + return text + + def _parse_voicemail(self, voicemailHtml): + splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml) + for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): + conv = Conversation() + conv.type = Conversation.TYPE_VOICEMAIL + conv.id = messageId.strip() + + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + conv.time = google_strptime(exactTimeText) + relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) + conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" + locationGroup = self._voicemailLocationRegex.search(messageHtml) + conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "") + + nameGroup = self._voicemailNameRegex.search(messageHtml) + conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "") + numberGroup = self._voicemailNumberRegex.search(messageHtml) + conv.number = numberGroup.group(1).strip() if numberGroup else "" + prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) + conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" + contactIdGroup = self._messagesContactIDRegex.search(messageHtml) + conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" + + messageGroups = self._voicemailMessageRegex.finditer(messageHtml) + messageParts = [ + self._interpret_voicemail_regex(group) + for group in messageGroups + ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), ) + message = Message() + message.body = messageParts + message.whoFrom = conv.name + message.when = conv.time.strftime("%I:%M %p") + conv.messages = (message, ) + + yield conv + + @staticmethod + def _interpret_sms_message_parts(fromPart, textPart, timePart): + text = MessageText() + text.accuracy = MessageText.ACCURACY_MEDIUM + text.text = unescape(textPart) + + message = Message() + message.body = (text, ) + message.whoFrom = fromPart + message.when = timePart + + return message + + def _parse_sms(self, smsHtml): + splitSms = self._seperateVoicemailsRegex.split(smsHtml) + for messageId, messageHtml in itergroup(splitSms[1:], 2): + conv = Conversation() + conv.type = Conversation.TYPE_SMS + conv.id = messageId.strip() + + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + conv.time = google_strptime(exactTimeText) + relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) + conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" + conv.location = "" + + nameGroup = self._voicemailNameRegex.search(messageHtml) + conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "") + numberGroup = self._voicemailNumberRegex.search(messageHtml) + conv.number = numberGroup.group(1).strip() if numberGroup else "" + prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) + conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" + contactIdGroup = self._messagesContactIDRegex.search(messageHtml) + conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else "" + + fromGroups = self._smsFromRegex.finditer(messageHtml) + fromParts = (group.group(1).strip() for group in fromGroups) + textGroups = self._smsTextRegex.finditer(messageHtml) + textParts = (group.group(1).strip() for group in textGroups) + timeGroups = self._smsTimeRegex.finditer(messageHtml) + timeParts = (group.group(1).strip() for group in timeGroups) + + messageParts = itertools.izip(fromParts, textParts, timeParts) + messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts] + conv.messages = messages + + yield conv + + @staticmethod + def _merge_conversation_sources(parsedMessages, json): + for message in parsedMessages: + jsonItem = json["messages"][message.id] + message.isRead = jsonItem["isRead"] + message.isSpam = jsonItem["isSpam"] + message.isTrash = jsonItem["isTrash"] + message.isArchived = "inbox" not in jsonItem["labels"] + yield message + + def _get_page(self, url, data = None, refererUrl = None): + headers = {} + if refererUrl is not None: + headers["Referer"] = refererUrl + + encodedData = urllib.urlencode(data) if data is not None else None + + try: + page = self._browser.download(url, encodedData, None, headers) + except urllib2.URLError, e: + _moduleLogger.error("Translating error: %s" % str(e)) + raise NetworkError("%s is not accesible" % url) + + return page + + def _get_page_with_token(self, url, data = None, refererUrl = None): + if data is None: + data = {} + data['_rnr_se'] = self._token + + page = self._get_page(url, data, refererUrl) + + return page + + def _parse_with_validation(self, page): + json = parse_json(page) + validate_response(json) + return json + + +_UNESCAPE_ENTITIES = { + """: '"', + " ": " ", + "'": "'", +} + + +def unescape(text): + plain = saxutils.unescape(text, _UNESCAPE_ENTITIES) + return plain + + +def google_strptime(time): + """ + Hack: Google always returns the time in the same locale. Sadly if the + local system's locale is different, there isn't a way to perfectly handle + the time. So instead we handle implement some time formatting + """ + abbrevTime = time[:-3] + parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M") + if time[-2] == "PN": + parsedTime += datetime.timedelta(hours=12) + return parsedTime + + +def itergroup(iterator, count, padValue = None): + """ + Iterate in groups of 'count' values. If there + aren't enough values, the last result is padded with + None. + + >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): + ... print tuple(val) + (1, 2, 3) + (4, 5, 6) + >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): + ... print list(val) + [1, 2, 3] + [4, 5, 6] + >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3): + ... print tuple(val) + (1, 2, 3) + (4, 5, 6) + (7, None, None) + >>> for val in itergroup("123456", 3): + ... print tuple(val) + ('1', '2', '3') + ('4', '5', '6') + >>> for val in itergroup("123456", 3): + ... print repr("".join(val)) + '123' + '456' + """ + paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1)) + nIterators = (paddedIterator, ) * count + return itertools.izip(*nIterators) + + +def safe_eval(s): + _TRUE_REGEX = re.compile("true") + _FALSE_REGEX = re.compile("false") + s = _TRUE_REGEX.sub("True", s) + s = _FALSE_REGEX.sub("False", s) + return eval(s, {}, {}) + + +def _fake_parse_json(flattened): + return safe_eval(flattened) + + +def _actual_parse_json(flattened): + return simplejson.loads(flattened) + + +if simplejson is None: + parse_json = _fake_parse_json +else: + parse_json = _actual_parse_json + + +def extract_payload(flatXml): + xmlTree = ElementTree.fromstring(flatXml) + + jsonElement = xmlTree.getchildren()[0] + flatJson = jsonElement.text + jsonTree = parse_json(flatJson) + + htmlElement = xmlTree.getchildren()[1] + flatHtml = htmlElement.text + + return jsonTree, flatHtml + + +def validate_response(response): + """ + Validates that the JSON response is A-OK + """ + try: + assert 'ok' in response and response['ok'] + except AssertionError: + raise RuntimeError('There was a problem with GV: %s' % response) + + +def guess_phone_type(number): + if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"): + return GVoiceBackend.PHONE_TYPE_GIZMO + else: + return GVoiceBackend.PHONE_TYPE_MOBILE + + +def get_sane_callback(backend): + """ + Try to set a sane default callback number on these preferences + 1) 1747 numbers ( Gizmo ) + 2) anything with gizmo in the name + 3) anything with computer in the name + 4) the first value + """ + numbers = backend.get_callback_numbers() + + priorityOrderedCriteria = [ + ("\+1747", None), + ("1747", None), + ("747", None), + (None, "gizmo"), + (None, "computer"), + (None, "sip"), + (None, None), + ] + + for numberCriteria, descriptionCriteria in priorityOrderedCriteria: + numberMatcher = None + descriptionMatcher = None + if numberCriteria is not None: + numberMatcher = re.compile(numberCriteria) + elif descriptionCriteria is not None: + descriptionMatcher = re.compile(descriptionCriteria, re.I) + + for number, description in numbers.iteritems(): + if numberMatcher is not None and numberMatcher.match(number) is None: + continue + if descriptionMatcher is not None and descriptionMatcher.match(description) is None: + continue + return number + + +def set_sane_callback(backend): + """ + Try to set a sane default callback number on these preferences + 1) 1747 numbers ( Gizmo ) + 2) anything with gizmo in the name + 3) anything with computer in the name + 4) the first value + """ + number = get_sane_callback(backend) + backend.set_callback_number(number) + + +def _is_not_special(name): + return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name + + +def to_dict(obj): + members = inspect.getmembers(obj) + return dict((name, value) for (name, value) in members if _is_not_special(name)) + + +def grab_debug_info(username, password): + cookieFile = os.path.join(".", "raw_cookies.txt") + try: + os.remove(cookieFile) + except OSError: + pass + + backend = GVoiceBackend(cookieFile) + browser = backend._browser + + _TEST_WEBPAGES = [ + ("forward", backend._forwardURL), + ("token", backend._tokenURL), + ("login", backend._loginURL), + ("isdnd", backend._isDndURL), + ("account", backend._XML_ACCOUNT_URL), + ("contacts", backend._XML_CONTACTS_URL), + + ("voicemail", backend._XML_VOICEMAIL_URL), + ("sms", backend._XML_SMS_URL), + + ("recent", backend._XML_RECENT_URL), + ("placed", backend._XML_PLACED_URL), + ("recieved", backend._XML_RECEIVED_URL), + ("missed", backend._XML_MISSED_URL), + ] + + # Get Pages + print "Grabbing pre-login pages" + for name, url in _TEST_WEBPAGES: + try: + page = browser.download(url) + except StandardError, e: + print e.message + continue + print "\tWriting to file" + with open("not_loggedin_%s.txt" % name, "w") as f: + f.write(page) + + # Login + print "Attempting login" + galxToken = backend._get_token() + loginSuccessOrFailurePage = backend._login(username, password, galxToken) + with open("loggingin.txt", "w") as f: + print "\tWriting to file" + f.write(loginSuccessOrFailurePage) + try: + 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) + if not loggedIn: + raise + + # Get Pages + print "Grabbing post-login pages" + for name, url in _TEST_WEBPAGES: + try: + page = browser.download(url) + except StandardError, e: + print str(e) + continue + print "\tWriting to file" + with open("loggedin_%s.txt" % name, "w") as f: + f.write(page) + + # Cookies + browser.save_cookies() + print "\tWriting cookies to file" + with open("cookies.txt", "w") as f: + f.writelines( + "%s: %s\n" % (c.name, c.value) + for c in browser._cookies + ) + + +def main(): + import sys + logging.basicConfig(level=logging.DEBUG) + args = sys.argv + if 3 <= len(args): + username = args[1] + password = args[2] + + grab_debug_info(username, password) + + +if __name__ == "__main__": + main() diff --git a/src/backends/merge_backend.py b/src/backends/merge_backend.py index 476a616..7cdd2e4 100644 --- a/src/backends/merge_backend.py +++ b/src/backends/merge_backend.py @@ -1,7 +1,7 @@ import logging -_moduleLogger = logging.getLogger("merge_backend") +_moduleLogger = logging.getLogger(__name__) class MergedAddressBook(object): diff --git a/src/dc_glade.py b/src/dc_glade.py deleted file mode 100755 index 14ffb02..0000000 --- a/src/dc_glade.py +++ /dev/null @@ -1,1006 +0,0 @@ -#!/usr/bin/env python - -""" -DialCentral - Front end for Google's GoogleVoice service. -Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -""" - - -from __future__ import with_statement - -import sys -import gc -import os -import threading -import base64 -import ConfigParser -import itertools -import shutil -import logging - -import gtk -import gtk.glade - -import constants -import hildonize -import gtk_toolbox - - -_moduleLogger = logging.getLogger("dc_glade") -PROFILE_STARTUP = False - - -def getmtime_nothrow(path): - try: - return os.path.getmtime(path) - except Exception: - return 0 - - -def display_error_message(msg): - error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg) - - def close(dialog, response): - dialog.destroy() - error_dialog.connect("response", close) - error_dialog.run() - - -class Dialcentral(object): - - _glade_files = [ - os.path.join(os.path.dirname(__file__), "dialcentral.glade"), - os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"), - '/usr/lib/dialcentral/dialcentral.glade', - ] - - KEYPAD_TAB = 0 - RECENT_TAB = 1 - MESSAGES_TAB = 2 - CONTACTS_TAB = 3 - ACCOUNT_TAB = 4 - - NULL_BACKEND = 0 - # 1 Was GrandCentral support so the gap was maintained for compatibility - GV_BACKEND = 2 - BACKENDS = (NULL_BACKEND, GV_BACKEND) - - def __init__(self): - self._initDone = False - self._connection = None - self._osso = None - self._deviceState = None - self._clipboard = gtk.clipboard_get() - - self._credentials = ("", "") - self._selectedBackendId = self.NULL_BACKEND - self._defaultBackendId = self.GV_BACKEND - self._phoneBackends = None - self._dialpads = None - self._accountViews = None - self._messagesViews = None - self._historyViews = None - self._contactsViews = None - self._alarmHandler = None - self._ledHandler = None - self._originalCurrentLabels = [] - self._fsContactsPath = os.path.join(constants._data_path_, "contacts") - - for path in self._glade_files: - if os.path.isfile(path): - self._widgetTree = gtk.glade.XML(path) - break - else: - display_error_message("Cannot find dialcentral.glade") - gtk.main_quit() - return - - self._window = self._widgetTree.get_widget("mainWindow") - self._notebook = self._widgetTree.get_widget("notebook") - errorBox = self._widgetTree.get_widget("errorEventBox") - errorDescription = self._widgetTree.get_widget("errorDescription") - errorClose = self._widgetTree.get_widget("errorClose") - self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose) - self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree) - self._smsEntryWindow = None - - self._isFullScreen = False - self.__isPortrait = False - self._app = hildonize.get_app_class()() - self._window = hildonize.hildonize_window(self._app, self._window) - hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry")) - hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry")) - - for scrollingWidgetName in ( - 'history_scrolledwindow', - 'message_scrolledwindow', - 'contacts_scrolledwindow', - "smsMessages_scrolledwindow", - ): - scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName) - assert scrollingWidget is not None, scrollingWidgetName - hildonize.hildonize_scrollwindow(scrollingWidget) - for scrollingWidgetName in ( - "smsMessage_scrolledEntry", - ): - scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName) - assert scrollingWidget is not None, scrollingWidgetName - hildonize.hildonize_scrollwindow_with_viewport(scrollingWidget) - - for buttonName in ( - "back", - "addressbookSelectButton", - "sendSmsButton", - "dialButton", - "callbackSelectButton", - "minutesEntryButton", - "clearcookies", - "phoneTypeSelection", - ): - button = self._widgetTree.get_widget(buttonName) - assert button is not None, buttonName - hildonize.set_button_thumb_selectable(button) - - menu = hildonize.hildonize_menu( - self._window, - self._widgetTree.get_widget("dialpad_menubar"), - ) - if not hildonize.GTK_MENU_USED: - button = gtk.Button("New Login") - button.connect("clicked", self._on_clearcookies_clicked) - menu.append(button) - - button = gtk.Button("Refresh") - button.connect("clicked", self._on_menu_refresh) - menu.append(button) - - menu.show_all() - - self._window.connect("key-press-event", self._on_key_press) - self._window.connect("window-state-event", self._on_window_state_change) - if not hildonize.IS_HILDON_SUPPORTED: - _moduleLogger.warning("No hildonization support") - - hildonize.set_application_name("%s" % constants.__pretty_app_name__) - - self._window.connect("destroy", self._on_close) - self._window.set_default_size(800, 300) - self._window.show_all() - - self._loginSink = gtk_toolbox.threaded_stage( - gtk_toolbox.comap( - self._attempt_login, - gtk_toolbox.null_sink(), - ) - ) - - if not PROFILE_STARTUP: - backgroundSetup = threading.Thread(target=self._idle_setup) - backgroundSetup.setDaemon(True) - backgroundSetup.start() - else: - self._idle_setup() - - def _idle_setup(self): - """ - If something can be done after the UI loads, push it here so it's not blocking the UI - """ - # Barebones UI handlers - try: - from backends import null_backend - import null_views - - self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()} - with gtk_toolbox.gtk_lock(): - self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)} - self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)} - self._historyViews = {self.NULL_BACKEND: null_views.CallHistoryView(self._widgetTree)} - self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)} - self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)} - - self._dialpads[self._selectedBackendId].enable() - self._accountViews[self._selectedBackendId].enable() - self._historyViews[self._selectedBackendId].enable() - self._messagesViews[self._selectedBackendId].enable() - self._contactsViews[self._selectedBackendId].enable() - except Exception, e: - with gtk_toolbox.gtk_lock(): - self._errorDisplay.push_exception() - - # Setup maemo specifics - try: - try: - import osso - except (ImportError, OSError): - osso = None - self._osso = None - self._deviceState = None - if osso is not None: - self._osso = osso.Context(constants.__app_name__, constants.__version__, False) - self._deviceState = osso.DeviceState(self._osso) - self._deviceState.set_device_state_callback(self._on_device_state_change, 0) - else: - _moduleLogger.warning("No device state support") - - 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): - alarm_handler = None - except Exception: - with gtk_toolbox.gtk_lock(): - self._errorDisplay.push_exception() - alarm_handler = None - if alarm_handler is None: - _moduleLogger.warning("No notification support") - if hildonize.IS_HILDON_SUPPORTED: - try: - import led_handler - self._ledHandler = led_handler.LedHandler() - except Exception, e: - _moduleLogger.exception('LED Handling failed: "%s"' % str(e)) - self._ledHandler = None - else: - self._ledHandler = None - - try: - import conic - except (ImportError, OSError): - conic = None - self._connection = None - if conic is not None: - self._connection = conic.Connection() - self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__) - self._connection.request_connection(conic.CONNECT_FLAG_NONE) - else: - _moduleLogger.warning("No connection support") - except Exception, e: - with gtk_toolbox.gtk_lock(): - self._errorDisplay.push_exception() - - # Setup costly backends - try: - from backends import gv_backend - from backends import file_backend - import gv_views - from backends import merge_backend - - with gtk_toolbox.gtk_lock(): - self._smsEntryWindow = gv_views.SmsEntryWindow(self._widgetTree, self._window, self._app) - try: - os.makedirs(constants._data_path_) - except OSError, e: - if e.errno != 17: - raise - gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt") - - self._phoneBackends.update({ - self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath), - }) - with gtk_toolbox.gtk_lock(): - unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay) - self._dialpads.update({ - self.GV_BACKEND: unifiedDialpad, - }) - self._accountViews.update({ - self.GV_BACKEND: gv_views.AccountInfo( - self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay - ), - }) - self._accountViews[self.GV_BACKEND].save_everything = self._save_settings - self._historyViews.update({ - self.GV_BACKEND: gv_views.CallHistoryView( - self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay - ), - }) - self._messagesViews.update({ - self.GV_BACKEND: gv_views.MessagesView( - self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay - ), - }) - self._contactsViews.update({ - self.GV_BACKEND: gv_views.ContactsView( - self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay - ), - }) - - fileBackend = file_backend.FilesystemAddressBookFactory(self._fsContactsPath) - - self._smsEntryWindow.send_sms = self._on_sms_clicked - self._smsEntryWindow.dial = self._on_dial_clicked - self._dialpads[self.GV_BACKEND].add_contact = self._add_contact - self._dialpads[self.GV_BACKEND].dial = self._on_dial_clicked - self._historyViews[self.GV_BACKEND].add_contact = self._add_contact - self._messagesViews[self.GV_BACKEND].add_contact = self._add_contact - self._contactsViews[self.GV_BACKEND].add_contact = self._add_contact - - addressBooks = [ - self._phoneBackends[self.GV_BACKEND], - fileBackend, - ] - mergedBook = merge_backend.MergedAddressBook(addressBooks, merge_backend.MergedAddressBook.basic_firtname_sorter) - self._contactsViews[self.GV_BACKEND].append(mergedBook) - self._contactsViews[self.GV_BACKEND].extend(addressBooks) - self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2]) - - callbackMapping = { - "on_paste": self._on_paste, - "on_refresh": self._on_menu_refresh, - "on_clearcookies_clicked": self._on_clearcookies_clicked, - "on_about_activate": self._on_about_activate, - } - if hildonize.GTK_MENU_USED: - self._widgetTree.signal_autoconnect(callbackMapping) - self._notebook.connect("switch-page", self._on_notebook_switch_page) - self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked) - - with gtk_toolbox.gtk_lock(): - self._originalCurrentLabels = [ - self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text() - for pageIndex in xrange(self._notebook.get_n_pages()) - ] - self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook) - self._notebookTapHandler.enable() - self._notebookTapHandler.on_tap = self._reset_tab_refresh - self._notebookTapHandler.on_hold = self._on_tab_refresh - self._notebookTapHandler.on_holding = self._set_tab_refresh - self._notebookTapHandler.on_cancel = self._reset_tab_refresh - - config = ConfigParser.SafeConfigParser() - config.read(constants._user_settings_) - with gtk_toolbox.gtk_lock(): - self.load_settings(config) - except Exception, e: - with gtk_toolbox.gtk_lock(): - self._errorDisplay.push_exception() - finally: - self._initDone = True - self._spawn_attempt_login() - - def _spawn_attempt_login(self, *args): - self._loginSink.send(args) - - def _attempt_login(self, force = False): - """ - @note This must be run outside of the UI lock - """ - try: - assert self._initDone, "Attempting login before app is fully loaded" - - serviceId = self.NULL_BACKEND - loggedIn = False - if not force and self._defaultBackendId != self.NULL_BACKEND: - with gtk_toolbox.gtk_lock(): - banner = hildonize.show_busy_banner_start(self._window, "Logging In...") - try: - self.refresh_session() - serviceId = self._defaultBackendId - loggedIn = True - except Exception, e: - _moduleLogger.exception('Session refresh failed with the following message "%s"' % str(e)) - finally: - with gtk_toolbox.gtk_lock(): - hildonize.show_busy_banner_end(banner) - - if not loggedIn: - loggedIn, serviceId = self._login_by_user() - - with gtk_toolbox.gtk_lock(): - self._change_loggedin_status(serviceId) - if loggedIn: - hildonize.show_information_banner(self._window, "Logged In") - else: - hildonize.show_information_banner(self._window, "Login Failed") - if not self._phoneBackends[self._defaultBackendId].get_callback_number(): - # subtle reminder to the users to configure things - self._notebook.set_current_page(self.ACCOUNT_TAB) - - except Exception, e: - with gtk_toolbox.gtk_lock(): - self._errorDisplay.push_exception() - - def refresh_session(self): - """ - @note Thread agnostic - """ - assert self._initDone, "Attempting login before app is fully loaded" - - loggedIn = False - if not loggedIn: - loggedIn = self._login_by_cookie() - if not loggedIn: - loggedIn = self._login_by_settings() - - if not loggedIn: - raise RuntimeError("Login Failed") - - def _login_by_cookie(self): - """ - @note Thread agnostic - """ - loggedIn = False - - isQuickLoginPossible = self._phoneBackends[self._defaultBackendId].is_quick_login_possible() - if self._credentials != ("", "") and isQuickLoginPossible: - if not loggedIn: - loggedIn = self._phoneBackends[self._defaultBackendId].is_authed() - - if loggedIn: - _moduleLogger.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId]) - else: - # If the cookies are bad, scratch them completely - self._phoneBackends[self._defaultBackendId].logout() - - return loggedIn - - def _login_by_settings(self): - """ - @note Thread agnostic - """ - if self._credentials == ("", ""): - # Don't bother with the settings if they are blank - return False - - username, password = self._credentials - loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password) - if loggedIn: - self._credentials = username, password - _moduleLogger.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId]) - return loggedIn - - def _login_by_user(self): - """ - @note This must be run outside of the UI lock - """ - loggedIn, (username, password) = False, self._credentials - tmpServiceId = self.GV_BACKEND - while not loggedIn: - with gtk_toolbox.gtk_lock(): - credentials = self._credentialsDialog.request_credentials( - defaultCredentials = self._credentials - ) - banner = hildonize.show_busy_banner_start(self._window, "Logging In...") - try: - username, password = credentials - loggedIn = self._phoneBackends[tmpServiceId].login(username, password) - finally: - with gtk_toolbox.gtk_lock(): - hildonize.show_busy_banner_end(banner) - - if loggedIn: - serviceId = tmpServiceId - self._credentials = username, password - _moduleLogger.info("Logged into %r through user request" % self._phoneBackends[serviceId]) - else: - # Hint to the user that they are not logged in - serviceId = self.NULL_BACKEND - self._notebook.set_current_page(self.ACCOUNT_TAB) - - return loggedIn, serviceId - - def _add_contact(self, *args, **kwds): - self._smsEntryWindow.add_contact(*args, **kwds) - - def _change_loggedin_status(self, newStatus): - oldStatus = self._selectedBackendId - if oldStatus == newStatus: - return - - _moduleLogger.debug("Changing from %s to %s" % (oldStatus, newStatus)) - self._dialpads[oldStatus].disable() - self._accountViews[oldStatus].disable() - self._historyViews[oldStatus].disable() - self._messagesViews[oldStatus].disable() - self._contactsViews[oldStatus].disable() - - self._dialpads[newStatus].enable() - self._accountViews[newStatus].enable() - self._historyViews[newStatus].enable() - self._messagesViews[newStatus].enable() - self._contactsViews[newStatus].enable() - - self._selectedBackendId = newStatus - - self._accountViews[self._selectedBackendId].update() - self._refresh_active_tab() - self._refresh_orientation() - - def load_settings(self, config): - """ - @note UI Thread - """ - try: - if not PROFILE_STARTUP: - self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active") - else: - self._defaultBackendId = self.NULL_BACKEND - blobs = ( - config.get(constants.__pretty_app_name__, "bin_blob_%i" % i) - for i in xrange(len(self._credentials)) - ) - creds = ( - base64.b64decode(blob) - for blob in blobs - ) - self._credentials = tuple(creds) - - if self._alarmHandler is not None: - self._alarmHandler.load_settings(config, "alarm") - - isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen") - if isFullscreen: - self._window.fullscreen() - - isPortrait = config.getboolean(constants.__pretty_app_name__, "portrait") - if isPortrait ^ self.__isPortrait: - if isPortrait: - orientation = gtk.ORIENTATION_VERTICAL - else: - orientation = gtk.ORIENTATION_HORIZONTAL - self.set_orientation(orientation) - except ConfigParser.NoOptionError, e: - _moduleLogger.exception( - "Settings file %s is missing section %s" % ( - constants._user_settings_, - e.section, - ), - ) - except ConfigParser.NoSectionError, e: - _moduleLogger.exception( - "Settings file %s is missing section %s" % ( - constants._user_settings_, - e.section, - ), - ) - - for backendId, view in itertools.chain( - self._dialpads.iteritems(), - self._accountViews.iteritems(), - self._messagesViews.iteritems(), - self._historyViews.iteritems(), - self._contactsViews.iteritems(), - ): - sectionName = "%s - %s" % (backendId, view.name()) - try: - view.load_settings(config, sectionName) - except ConfigParser.NoOptionError, e: - _moduleLogger.exception( - "Settings file %s is missing section %s" % ( - constants._user_settings_, - e.section, - ), - ) - except ConfigParser.NoSectionError, e: - _moduleLogger.exception( - "Settings file %s is missing section %s" % ( - constants._user_settings_, - e.section, - ), - ) - - def save_settings(self, config): - """ - @note Thread Agnostic - """ - # Because we now only support GVoice, if there are user credentials, - # always assume its using the GVoice backend - if self._credentials[0] and self._credentials[1]: - backend = self.GV_BACKEND - else: - backend = self.NULL_BACKEND - - config.add_section(constants.__pretty_app_name__) - config.set(constants.__pretty_app_name__, "active", str(backend)) - config.set(constants.__pretty_app_name__, "portrait", str(self.__isPortrait)) - config.set(constants.__pretty_app_name__, "fullscreen", str(self._isFullScreen)) - for i, value in enumerate(self._credentials): - blob = base64.b64encode(value) - config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob) - config.add_section("alarm") - if self._alarmHandler is not None: - self._alarmHandler.save_settings(config, "alarm") - - for backendId, view in itertools.chain( - self._dialpads.iteritems(), - self._accountViews.iteritems(), - self._messagesViews.iteritems(), - self._historyViews.iteritems(), - self._contactsViews.iteritems(), - ): - sectionName = "%s - %s" % (backendId, view.name()) - config.add_section(sectionName) - view.save_settings(config, sectionName) - - def _save_settings(self): - """ - @note Thread Agnostic - """ - config = ConfigParser.SafeConfigParser() - self.save_settings(config) - with open(constants._user_settings_, "wb") as configFile: - config.write(configFile) - - def _refresh_active_tab(self): - pageIndex = self._notebook.get_current_page() - if pageIndex == self.CONTACTS_TAB: - self._contactsViews[self._selectedBackendId].update(force=True) - elif pageIndex == self.RECENT_TAB: - self._historyViews[self._selectedBackendId].update(force=True) - elif pageIndex == self.MESSAGES_TAB: - self._messagesViews[self._selectedBackendId].update(force=True) - - if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB): - if self._ledHandler is not None: - self._ledHandler.off() - - def set_orientation(self, orientation): - if orientation == gtk.ORIENTATION_VERTICAL: - hildonize.window_to_portrait(self._window) - self._notebook.set_property("tab-pos", gtk.POS_BOTTOM) - self.__isPortrait = True - elif orientation == gtk.ORIENTATION_HORIZONTAL: - hildonize.window_to_landscape(self._window) - self._notebook.set_property("tab-pos", gtk.POS_LEFT) - self.__isPortrait = False - else: - raise NotImplementedError(orientation) - - def get_orientation(self): - return gtk.ORIENTATION_VERTICAL if self.__isPortrait else gtk.ORIENTATION_HORIZONTAL - - def _toggle_rotate(self): - if self.__isPortrait: - self.set_orientation(gtk.ORIENTATION_HORIZONTAL) - else: - self.set_orientation(gtk.ORIENTATION_VERTICAL) - - def _refresh_orientation(self): - """ - Mostly meant to be used when switching backends - """ - if self.__isPortrait: - self.set_orientation(gtk.ORIENTATION_VERTICAL) - else: - self.set_orientation(gtk.ORIENTATION_HORIZONTAL) - - @gtk_toolbox.log_exception(_moduleLogger) - def _on_close(self, *args, **kwds): - try: - if self._initDone: - self._save_settings() - - try: - self._deviceState.close() - except AttributeError: - pass # Either None or close was removed (in Fremantle) - try: - self._osso.close() - except AttributeError: - pass # Either None or close was removed (in Fremantle) - finally: - gtk.main_quit() - - def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData): - """ - For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us. - For system_inactivity, we have no background tasks to pause - - @note Hildon specific - """ - try: - if memory_low: - for backendId in self.BACKENDS: - self._phoneBackends[backendId].clear_caches() - self._contactsViews[self._selectedBackendId].clear_caches() - gc.collect() - - if save_unsaved_data or shutdown: - self._save_settings() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_connection_change(self, connection, event, magicIdentifier): - """ - @note Hildon specific - """ - try: - import conic - - status = event.get_status() - error = event.get_error() - iap_id = event.get_iap_id() - bearer = event.get_bearer_type() - - if status == conic.STATUS_CONNECTED: - if self._initDone: - self._spawn_attempt_login() - elif status == conic.STATUS_DISCONNECTED: - if self._initDone: - self._defaultBackendId = self._selectedBackendId - self._change_loggedin_status(self.NULL_BACKEND) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_window_state_change(self, widget, event, *args): - """ - @note Hildon specific - """ - try: - if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: - self._isFullScreen = True - else: - self._isFullScreen = False - except Exception, e: - self._errorDisplay.push_exception() - - def _on_key_press(self, widget, event, *args): - """ - @note Hildon specific - """ - RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter) - try: - if ( - event.keyval == gtk.keysyms.F6 or - event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK - ): - if self._isFullScreen: - self._window.unfullscreen() - else: - self._window.fullscreen() - elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK: - with open(constants._user_logpath_, "r") as f: - logLines = f.xreadlines() - log = "".join(logLines) - self._clipboard.set_text(str(log)) - elif ( - event.keyval in (gtk.keysyms.w, gtk.keysyms.q) and - event.get_state() & gtk.gdk.CONTROL_MASK - ): - self._window.destroy() - elif event.keyval == gtk.keysyms.o and event.get_state() & gtk.gdk.CONTROL_MASK: - self._toggle_rotate() - return True - elif event.keyval == gtk.keysyms.r and event.get_state() & gtk.gdk.CONTROL_MASK: - self._refresh_active_tab() - elif event.keyval == gtk.keysyms.i and event.get_state() & gtk.gdk.CONTROL_MASK: - self._import_contacts() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_clearcookies_clicked(self, *args): - try: - self._phoneBackends[self._selectedBackendId].logout() - self._accountViews[self._selectedBackendId].clear() - self._historyViews[self._selectedBackendId].clear() - self._messagesViews[self._selectedBackendId].clear() - self._contactsViews[self._selectedBackendId].clear() - self._change_loggedin_status(self.NULL_BACKEND) - - self._spawn_attempt_login(True) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_notebook_switch_page(self, notebook, page, pageIndex): - try: - self._reset_tab_refresh() - - didRecentUpdate = False - didMessagesUpdate = False - - if pageIndex == self.RECENT_TAB: - didRecentUpdate = self._historyViews[self._selectedBackendId].update() - elif pageIndex == self.MESSAGES_TAB: - didMessagesUpdate = self._messagesViews[self._selectedBackendId].update() - elif pageIndex == self.CONTACTS_TAB: - self._contactsViews[self._selectedBackendId].update() - elif pageIndex == self.ACCOUNT_TAB: - self._accountViews[self._selectedBackendId].update() - - if didRecentUpdate or didMessagesUpdate: - if self._ledHandler is not None: - self._ledHandler.off() - except Exception, e: - self._errorDisplay.push_exception() - - def _set_tab_refresh(self, *args): - try: - pageIndex = self._notebook.get_current_page() - child = self._notebook.get_nth_page(pageIndex) - self._notebook.get_tab_label(child).set_text("Refresh?") - except Exception, e: - self._errorDisplay.push_exception() - return False - - def _reset_tab_refresh(self, *args): - try: - pageIndex = self._notebook.get_current_page() - child = self._notebook.get_nth_page(pageIndex) - self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex]) - except Exception, e: - self._errorDisplay.push_exception() - return False - - def _on_tab_refresh(self, *args): - try: - self._refresh_active_tab() - self._reset_tab_refresh() - except Exception, e: - self._errorDisplay.push_exception() - return False - - def _on_sms_clicked(self, numbers, message): - try: - assert numbers, "No number specified" - assert message, "Empty message" - self.refresh_session() - try: - loggedIn = self._phoneBackends[self._selectedBackendId].is_authed() - except Exception, e: - loggedIn = False - self._errorDisplay.push_exception() - return - - if not loggedIn: - self._errorDisplay.push_message( - "Backend link with GoogleVoice is not working, please try again" - ) - return - - dialed = False - try: - self._phoneBackends[self._selectedBackendId].send_sms(numbers, message) - hildonize.show_information_banner(self._window, "Sending to %s" % ", ".join(numbers)) - _moduleLogger.info("Sending SMS to %r" % numbers) - dialed = True - except Exception, e: - self._errorDisplay.push_exception() - - if dialed: - self._dialpads[self._selectedBackendId].clear() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_dial_clicked(self, number): - try: - assert number, "No number to call" - self.refresh_session() - try: - loggedIn = self._phoneBackends[self._selectedBackendId].is_authed() - except Exception, e: - loggedIn = False - self._errorDisplay.push_exception() - return - - if not loggedIn: - self._errorDisplay.push_message( - "Backend link with GoogleVoice is not working, please try again" - ) - return - - dialed = False - try: - assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified" - self._phoneBackends[self._selectedBackendId].call(number) - hildonize.show_information_banner(self._window, "Calling %s" % number) - _moduleLogger.info("Calling %s" % number) - dialed = True - except Exception, e: - self._errorDisplay.push_exception() - - if dialed: - self._dialpads[self._selectedBackendId].clear() - except Exception, e: - self._errorDisplay.push_exception() - - def _import_contacts(self): - csvFilter = gtk.FileFilter() - csvFilter.set_name("Contacts") - csvFilter.add_pattern("*.csv") - importFileChooser = gtk.FileChooserDialog( - title="Contacts", - parent=self._window, - ) - importFileChooser.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) - importFileChooser.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) - - importFileChooser.set_property("filter", csvFilter) - userResponse = importFileChooser.run() - importFileChooser.hide() - if userResponse == gtk.RESPONSE_OK: - filename = importFileChooser.get_filename() - shutil.copy2(filename, self._fsContactsPath) - - def _on_menu_refresh(self, *args): - try: - self._refresh_active_tab() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_paste(self, *args): - try: - contents = self._clipboard.wait_for_text() - if contents is not None: - self._dialpads[self._selectedBackendId].set_number(contents) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_about_activate(self, *args): - try: - dlg = gtk.AboutDialog() - dlg.set_name(constants.__pretty_app_name__) - dlg.set_version("%s-%d" % (constants.__version__, constants.__build__)) - dlg.set_copyright("Copyright 2008 - LGPL") - dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account. This application is not affiliated with Google in any way") - dlg.set_website("http://gc-dialer.garage.maemo.org/") - dlg.set_authors(["", "Eric Warnke ", "Ed Page "]) - dlg.run() - dlg.destroy() - except Exception, e: - self._errorDisplay.push_exception() - - -def run_doctest(): - import doctest - - failureCount, testCount = doctest.testmod() - if not failureCount: - print "Tests Successful" - sys.exit(0) - else: - sys.exit(1) - - -def run_dialpad(): - gtk.gdk.threads_init() - - handle = Dialcentral() - if not PROFILE_STARTUP: - gtk.main() - - -class DummyOptions(object): - - def __init__(self): - self.test = False - - -if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) - try: - if len(sys.argv) > 1: - try: - import optparse - except ImportError: - optparse = None - - if optparse is not None: - parser = optparse.OptionParser() - parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests") - (commandOptions, commandArgs) = parser.parse_args() - else: - commandOptions = DummyOptions() - commandArgs = [] - - if commandOptions.test: - run_doctest() - else: - run_dialpad() - finally: - logging.shutdown() diff --git a/src/dialcentral.glade b/src/dialcentral.glade deleted file mode 100644 index 97ca390..0000000 --- a/src/dialcentral.glade +++ /dev/null @@ -1,1406 +0,0 @@ - - - - - - Dialer - 800 - 480 - - - True - - - True - - - True - _Login - True - - - - - - True - Paste - True - - - - - - True - Refresh - True - - - - - - True - _About - True - - - - - - False - 0 - - - - - True - - - True - - - True - gtk-dialog-error - - - False - False - 0 - - - - - True - Unknown Error - True - end - True - - - 1 - - - - - True - gtk-close - - - False - False - 2 - - - - - - - 1 - - - - - True - True - left - False - True - - - True - - - True - - - True - True - True - False - - - - True - - - True - + - 1 - gtk-add - - - 0 - - - - - - - False - 0 - - - - - 50 - True - <span size="35000" weight="bold"></span> - True - center - - - 1 - - - - - gtk-go-back - True - True - True - True - False - - - - False - 2 - - - - - False - False - 0 - - - - - True - 4 - 3 - True - - - True - False - False - False - - - - - True - <span size="33000" weight="bold">1</span> -<span size="9000"> </span> - True - - - - - - - True - False - False - False - - - - - - - - True - <span size="30000" weight="bold">2</span> -<span size="12000">ABC</span> - True - center - - - - - 1 - 2 - - - - - True - False - False - False - - - - - - - - True - <span size="30000" weight="bold" stretch="ultraexpanded">3</span> -<span size="12000">DEF</span> - True - center - - - - - 2 - 3 - - - - - True - False - False - False - - - - - - - - True - <span size="30000" weight="bold">4</span> -<span size="12000">GHI</span> - True - center - - - - - 1 - 2 - - - - - True - False - False - False - - - - - - - - True - <span size="30000" weight="bold">5</span> -<span size="12000">JKL</span> - True - center - - - - - 1 - 2 - 1 - 2 - - - - - True - False - False - False - - - - - - - - True - <span size="30000" weight="bold">6</span> -<span size="12000">MNO</span> - True - center - - - - - 2 - 3 - 1 - 2 - - - - - True - False - False - False - - - - - - - - - True - <span size="30000" weight="bold">7</span> -<span size="12000">PQRS</span> - True - center - - - - - 2 - 3 - - - - - True - False - False - False - - - - - - - - True - <span size="30000" weight="bold">8</span> -<span size="12000">TUV</span> - True - center - - - - - 1 - 2 - 2 - 3 - - - - - True - False - False - False - - - - - - - - - True - <span size="30000" weight="bold">9</span> -<span size="12000">WXYZ</span> - True - center - - - - - 2 - 3 - 2 - 3 - - - - - True - False - False - False - - - - - True - <span size="33000" weight="bold">0</span> - True - center - - - - - 1 - 2 - 3 - 4 - - - - - True - False - False - - - - True - - - True - 1 - gtk-yes - - - 0 - - - - - True - 0 - 5 - <span size="17000" weight="bold">Call</span> - True - - - 1 - - - - - - - 2 - 3 - 3 - 4 - - - - - True - True - True - - - - True - - - True - 1 - gtk-select-font - - - 0 - - - - - True - 0 - 5 - <span size="17000" weight="bold">SMS</span> - True - - - 1 - - - - - - - 3 - 4 - - - - - 1 - - - - - True - False - - - - - 30 - True - Keypad - - - True - False - tab - - - - - True - vertical - - - All - True - True - True - - - False - 0 - - - - - True - True - never - - - True - True - True - horizontal - True - - - - - 1 - - - - - 1 - True - False - - - - - 30 - True - History - - - 1 - True - False - tab - - - - - True - vertical - - - True - - - True - True - True - - - 0 - - - - - True - True - True - - - 1 - - - - - False - 0 - - - - - True - True - never - automatic - - - True - queue - - - True - vertical - - - True - True - True - horizontal - True - - - 0 - - - - - - - - - 1 - - - - - 2 - - - - - True - Messages - - - 2 - True - False - tab - - - - - True - vertical - - - True - True - True - - - False - 0 - - - - - True - True - never - - - True - True - False - True - True - horizontal - True - - - - - 1 - - - - - 3 - - - - - True - Contacts - - - 3 - True - False - tab - - - - - True - 11 - 7 - 2 - - - True - 0 - No Number Available - True - - - 1 - 2 - GTK_FILL - - - - - - True - 0 - 0 - 10 - Account Number: - - - - - - - - True - 0 - 0 - 10 - Callback Number: - - - 1 - 2 - - - - - - True - 0 - - - 2 - 3 - - - - - True - 0 - - - 4 - 5 - - - - - True - vertical - - - Missed Calls - True - True - False - True - - - 0 - - - - - Voicemail - True - True - False - True - - - 1 - - - - - SMS - True - True - False - True - - - 2 - - - - - 1 - 2 - 4 - 5 - - - - - Notifications - True - True - False - 0 - True - - - 3 - 4 - GTK_FILL - - - - - - New Login - True - True - True - False - - - 1 - 2 - 6 - 7 - GTK_FILL - - - - - - True - 0 - - - 5 - 6 - - - - - True - True - True - - - 1 - 2 - 3 - 4 - GTK_FILL - - - - - True - True - True - - - 1 - 2 - 1 - 2 - - - - - - - - - - - - - - - 4 - True - False - - - - - 30 - True - Account - - - 4 - False - tab - - - - - 2 - - - - - - - 5 - Login - False - True - center-on-parent - True - dialog - True - True - False - mainWindow - False - - - True - 2 - - - True - - - False - False - 1 - - - - - True - 2 - 2 - - - True - Username - - - - - True - Password - - - 1 - 2 - - - - - True - True - - - 1 - 2 - - - - - True - True - False - - - 1 - 2 - 1 - 2 - - - - - 2 - - - - - True - end - - - gtk-cancel - True - True - True - True - - - False - False - 0 - - - - - gtk-ok - True - True - True - True - True - - - False - False - 1 - - - - - False - end - 0 - - - - - - - SMS Entry - - - True - vertical - - - True - - - True - - - True - gtk-dialog-error - - - False - False - 0 - - - - - True - Unknown Error - True - end - True - - - 1 - - - - - True - gtk-close - - - False - False - 2 - - - - - - - False - 0 - - - - - True - vertical - - - True - True - automatic - automatic - - - True - queue - - - True - vertical - - - True - True - - - False - 0 - - - - - True - - - False - 1 - - - - - True - vertical - 3 - - - - - - False - 2 - - - - - True - 5 - 0 - none - - - True - vertical - - - True - True - never - automatic - - - True - True - word - - - - - 0 - - - - - - - <b>frame1</b> - True - - - label_item - - - - - 3 - - - - - - - - - 0 - - - - - 1 - - - - - True - 5 - 0 - none - - - True - 5 - - - True - - - True - Letters: - True - - - False - False - 0 - - - - - True - 0 - True - - - False - 1 - - - - - False - 0 - - - - - True - 0.47999998927116394 - 0.49000000953674316 - - - 1 - - - - - No Phone Types Available - True - True - True - - - False - 2 - - - - - True - 0.47999998927116394 - 0.49000000953674316 - - - 3 - - - - - True - True - True - - - True - - - True - gtk-select-font - - - 0 - - - - - True - SMS - - - 1 - - - - - - - False - False - 4 - - - - - True - True - True - - - True - - - True - gtk-apply - - - 0 - - - - - True - Dial - - - 1 - - - - - - - False - False - 5 - - - - - - - <b>frame2</b> - True - - - label_item - - - - - False - end - 2 - - - - - - diff --git a/src/dialcentral.py b/src/dialcentral.py index 19191a8..8f3fe12 100755 --- a/src/dialcentral.py +++ b/src/dialcentral.py @@ -1,31 +1,45 @@ -#!/usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Copyright (C) 2007 Christoph Würstle +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. +""" + import os import sys import logging -_moduleLogger = logging.getLogger("dialcentral") -sys.path.insert(0,"/opt/dialcentral/lib") +_moduleLogger = logging.getLogger(__name__) +sys.path.append("/opt/dialcentral/lib") import constants -import dc_glade - - -try: - os.makedirs(constants._data_path_) -except OSError, e: - if e.errno != 17: - raise - -logging.basicConfig(level=logging.DEBUG, filename=constants._user_logpath_) -_moduleLogger.info("Dialcentral %s-%s" % (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: - dc_glade.run_dialpad() -finally: - logging.shutdown() +import dialcentral_qt + + +if __name__ == "__main__": + try: + os.makedirs(constants._data_path_) + except OSError, e: + if e.errno != 17: + raise + + try: + os.makedirs(constants._cache_path_) + except OSError, e: + if e.errno != 17: + raise + + logFormat = '(%(asctime)s) %(levelname)-5s %(threadName)s.%(name)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() diff --git a/src/dialcentral_qt.py b/src/dialcentral_qt.py new file mode 100755 index 0000000..aa1d770 --- /dev/null +++ b/src/dialcentral_qt.py @@ -0,0 +1,447 @@ +#!/usr/bin/env python +# -*- coding: UTF8 -*- + +from __future__ import with_statement + +import sys +import os +import shutil +import simplejson +import logging + +from PyQt4 import QtGui +from PyQt4 import QtCore + +import constants +import maeqt +from util import misc as misc_utils + + +_moduleLogger = logging.getLogger(__name__) + + +IS_MAEMO = True + + +class Dialcentral(object): + + _DATA_PATHS = [ + os.path.dirname(__file__), + os.path.join(os.path.dirname(__file__), "../data"), + os.path.join(os.path.dirname(__file__), "../lib"), + '/usr/share/%s' % constants.__app_name__, + '/usr/lib/%s' % constants.__app_name__, + ] + + def __init__(self, app): + self._app = app + self._recent = [] + self._hiddenCategories = set() + self._hiddenUnits = {} + self._clipboard = QtGui.QApplication.clipboard() + + self._mainWindow = None + + self._fullscreenAction = QtGui.QAction(None) + self._fullscreenAction.setText("Fullscreen") + self._fullscreenAction.setCheckable(True) + self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter")) + self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen) + + self._logAction = QtGui.QAction(None) + self._logAction.setText("Log") + self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l")) + self._logAction.triggered.connect(self._on_log) + + self._quitAction = QtGui.QAction(None) + self._quitAction.setText("Quit") + self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q")) + self._quitAction.triggered.connect(self._on_quit) + + self._app.lastWindowClosed.connect(self._on_app_quit) + self.load_settings() + + self._mainWindow = MainWindow(None, self) + self._mainWindow.window.destroyed.connect(self._on_child_close) + + def load_settings(self): + try: + with open(constants._user_settings_, "r") as settingsFile: + settings = simplejson.load(settingsFile) + except IOError, e: + _moduleLogger.info("No settings") + settings = {} + except ValueError: + _moduleLogger.info("Settings were corrupt") + settings = {} + + self._fullscreenAction.setChecked(settings.get("isFullScreen", False)) + + def save_settings(self): + settings = { + "isFullScreen": self._fullscreenAction.isChecked(), + } + with open(constants._user_settings_, "w") as settingsFile: + simplejson.dump(settings, settingsFile) + + @property + def fullscreenAction(self): + return self._fullscreenAction + + @property + def logAction(self): + return self._logAction + + @property + def quitAction(self): + return self._quitAction + + def _close_windows(self): + if self._mainWindow is not None: + self._mainWindow.window.destroyed.disconnect(self._on_child_close) + self._mainWindow.close() + self._mainWindow = None + + @misc_utils.log_exception(_moduleLogger) + def _on_app_quit(self, checked = False): + self.save_settings() + + @misc_utils.log_exception(_moduleLogger) + def _on_child_close(self, obj = None): + self._mainWindow = None + + @misc_utils.log_exception(_moduleLogger) + def _on_toggle_fullscreen(self, checked = False): + for window in self._walk_children(): + window.set_fullscreen(checked) + + @misc_utils.log_exception(_moduleLogger) + def _on_log(self, checked = False): + with open(constants._user_logpath_, "r") as f: + logLines = f.xreadlines() + log = "".join(logLines) + self._clipboard.setText(log) + + @misc_utils.log_exception(_moduleLogger) + def _on_quit(self, checked = False): + self._close_windows() + + +class QErrorDisplay(object): + + def __init__(self): + self._messages = [] + + errorIcon = maeqt.get_theme_icon(("dialog-error", "app_install_error", "gtk-dialog-error")) + self._severityIcon = errorIcon.pixmap(32, 32) + self._severityLabel = QtGui.QLabel() + self._severityLabel.setPixmap(self._severityIcon) + + self._message = QtGui.QLabel() + self._message.setText("Boo") + + closeIcon = maeqt.get_theme_icon(("window-close", "general_close", "gtk-close")) + self._closeLabel = QtGui.QPushButton(closeIcon, "") + self._closeLabel.clicked.connect(self._on_close) + + self._controlLayout = QtGui.QHBoxLayout() + self._controlLayout.addWidget(self._severityLabel) + self._controlLayout.addWidget(self._message) + self._controlLayout.addWidget(self._closeLabel) + + self._topLevelLayout = QtGui.QHBoxLayout() + self._topLevelLayout.addLayout(self._controlLayout) + self._widget = QtGui.QWidget() + self._widget.setLayout(self._topLevelLayout) + self._hide_message() + + @property + def toplevel(self): + return self._widget + + def push_message(self, message): + self._messages.append(message) + if 1 == len(self._messages): + self._show_message(message) + + def push_exception(self): + userMessage = str(sys.exc_info()[1]) + _moduleLogger.exception(userMessage) + self.push_message(userMessage) + + def pop_message(self): + del self._messages[0] + if 0 == len(self._messages): + self._hide_message() + else: + self._message.setText(self._messages[0]) + + def _on_close(self, *args): + self.pop_message() + + def _show_message(self, message): + self._message.setText(message) + self._widget.show() + + def _hide_message(self): + self._message.setText("") + self._widget.hide() + + +class CredentialsDialog(object): + + def __init__(self): + self._usernameField = QtGui.QLineEdit() + self._passwordField = QtGui.QLineEdit() + self._passwordField.setEchoMode(QtGui.QLineEdit.PasswordEchoOnEdit) + + self._credLayout = QtGui.QGridLayout() + self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0) + self._credLayout.addWidget(self._usernameField, 0, 1) + self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0) + self._credLayout.addWidget(self._passwordField, 1, 1) + + self._loginButton = QtGui.QPushButton("&Login") + 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._layout.addLayout(self._buttonLayout) + + centralWidget = QtGui.QWidget() + centralWidget.setLayout(self._layout) + + self._dialog = QtGui.QDialog() + self._dialog.setWindowTitle("Login") + self._dialog.setCentralWidget(centralWidget) + maeqt.set_autorient(self._dialog, True) + self._buttonLayout.accepted.connect(self._dialog.accept) + self._buttonLayout.rejected.connect(self._dialog.reject) + + def run(self, defaultUsername, defaultPassword, parent=None): + self._dialog.setParent(parent) + self._usernameField.setText(defaultUsername) + self._passwordField.setText(defaultPassword) + + response = self._dialog.exec_() + if response == QtGui.QDialog.Accepted: + return str(self._usernameField.text()), str(self._passwordField.text()) + elif response == QtGui.QDialog.Rejected: + raise RuntimeError("Login Cancelled") + + +class AccountDialog(object): + + def __init__(self): + self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET") + self._clearButton = QtGui.QPushButton("Clear Account") + self._clearButton.clicked.connect(self._on_clear) + self._doClear = False + + self._credLayout = QtGui.QGridLayout() + self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0) + self._credLayout.addWidget(self._accountNumberLabel, 0, 1) + self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0) + self._credLayout.addWidget(self._clearButton, 2, 1) + + self._loginButton = QtGui.QPushButton("&Login") + 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._layout.addLayout(self._buttonLayout) + + centralWidget = QtGui.QWidget() + centralWidget.setLayout(self._layout) + + self._dialog = QtGui.QDialog() + self._dialog.setWindowTitle("Login") + self._dialog.setCentralWidget(centralWidget) + maeqt.set_autorient(self._dialog, True) + self._buttonLayout.accepted.connect(self._dialog.accept) + self._buttonLayout.rejected.connect(self._dialog.reject) + + @property + def doClear(self): + return self._doClear + + accountNumber = property( + lambda self: str(self._accountNumberLabel.text()), + lambda self, num: self._accountNumberLabel.setText(num), + ) + + def run(self, defaultUsername, defaultPassword, parent=None): + self._doClear = False + self._dialog.setParent(parent) + self._usernameField.setText(defaultUsername) + self._passwordField.setText(defaultPassword) + + response = self._dialog.exec_() + if response == QtGui.QDialog.Accepted: + return str(self._usernameField.text()), str(self._passwordField.text()) + elif response == QtGui.QDialog.Rejected: + raise RuntimeError("Login Cancelled") + + def _on_clear(self, checked = False): + self._doClear = True + self._dialog.accept() + + +class MainWindow(object): + + KEYPAD_TAB = 0 + RECENT_TAB = 1 + MESSAGES_TAB = 2 + CONTACTS_TAB = 3 + ACCOUNT_TAB = 4 + + def __init__(self, parent, app): + self._fsContactsPath = os.path.join(constants._data_path_, "contacts") + self._app = app + + self._errorDisplay = QErrorDisplay() + + self._tabs = QtGui.QTabWidget() + if maeqt.screen_orientation() == QtCore.Qt.Vertical: + self._tabs.setTabPosition(QtGui.QTabWidget.South) + else: + self._tabs.setTabPosition(QtGui.QTabWidget.West) + + self._layout = QtGui.QVBoxLayout() + self._layout.addWidget(self._errorDisplay.toplevel) + self._layout.addWidget(self._tabs) + + centralWidget = QtGui.QWidget() + centralWidget.setLayout(self._layout) + + self._window = QtGui.QMainWindow(parent) + self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) + maeqt.set_autorient(self._window, True) + maeqt.set_stackable(self._window, True) + self._window.setWindowTitle("%s" % constants.__pretty_app_name__) + self._window.setCentralWidget(centralWidget) + + self._loginTabAction = QtGui.QAction(None) + self._loginTabAction.setText("Login") + self._loginTabAction.triggered.connect(self._on_login) + + self._importTabAction = QtGui.QAction(None) + self._importTabAction.setText("Import") + self._importTabAction.triggered.connect(self._on_import) + + self._refreshTabAction = QtGui.QAction(None) + self._refreshTabAction.setText("Refresh") + self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r")) + self._refreshTabAction.triggered.connect(self._on_refresh) + + self._closeWindowAction = QtGui.QAction(None) + self._closeWindowAction.setText("Close") + self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w")) + self._closeWindowAction.triggered.connect(self._on_close_window) + + if IS_MAEMO: + fileMenu = self._window.menuBar().addMenu("&File") + fileMenu.addAction(self._loginTabAction) + fileMenu.addAction(self._refreshTabAction) + + toolsMenu = self._window.menuBar().addMenu("&Tools") + toolsMenu.addAction(self._importTabAction) + + self._window.addAction(self._closeWindowAction) + self._window.addAction(self._app.quitAction) + self._window.addAction(self._app.fullscreenAction) + else: + fileMenu = self._window.menuBar().addMenu("&File") + fileMenu.addAction(self._loginTabAction) + fileMenu.addAction(self._refreshTabAction) + fileMenu.addAction(self._closeWindowAction) + fileMenu.addAction(self._app.quitAction) + + viewMenu = self._window.menuBar().addMenu("&View") + viewMenu.addAction(self._app.fullscreenAction) + + toolsMenu = self._window.menuBar().addMenu("&Tools") + toolsMenu.addAction(self._importTabAction) + + self._window.addAction(self._app.logAction) + + self.set_fullscreen(self._app.fullscreenAction.isChecked()) + self._window.show() + + @property + def window(self): + return self._window + + def walk_children(self): + return () + + def show(self): + self._window.show() + for child in self.walk_children(): + child.show() + + def hide(self): + for child in self.walk_children(): + child.hide() + self._window.hide() + + def close(self): + for child in self.walk_children(): + child.window.destroyed.disconnect(self._on_child_close) + child.close() + self._window.close() + + def set_fullscreen(self, isFullscreen): + if isFullscreen: + self._window.showFullScreen() + else: + self._window.showNormal() + for child in self.walk_children(): + child.set_fullscreen(isFullscreen) + + def _populate_tab(self, index): + pass + + @misc_utils.log_exception(_moduleLogger) + def _on_login(self, checked = True): + pass + + @misc_utils.log_exception(_moduleLogger) + def _on_tab_changed(self, index): + self._populate_tab(index) + + @misc_utils.log_exception(_moduleLogger) + def _on_refresh(self, checked = True): + index = self._tabs.currentIndex() + self._populate_tab(index) + + @misc_utils.log_exception(_moduleLogger) + def _on_import(self, checked = True): + csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)") + if not csvName: + return + shutil.copy2(csvName, self._fsContactsPath) + + @misc_utils.log_exception(_moduleLogger) + def _on_close_window(self, checked = True): + self.close() + + +def run(): + app = QtGui.QApplication([]) + handle = Dialcentral(app) + return app.exec_() + + +if __name__ == "__main__": + logging.basicConfig(level = logging.DEBUG) + try: + os.makedirs(constants._data_path_) + except OSError, e: + if e.errno != 17: + raise + + val = run() + sys.exit(val) diff --git a/src/gtk_toolbox.py b/src/gtk_toolbox.py deleted file mode 100644 index 769e19b..0000000 --- a/src/gtk_toolbox.py +++ /dev/null @@ -1,760 +0,0 @@ -#!/usr/bin/python - -from __future__ import with_statement - -import os -import errno -import sys -import time -import itertools -import functools -import contextlib -import logging -import threading -import Queue - -import gobject -import gtk - - -_moduleLogger = logging.getLogger(__name__) - - -def get_screen_orientation(): - width, height = gtk.gdk.get_default_root_window().get_size() - if width < height: - return gtk.ORIENTATION_VERTICAL - else: - return gtk.ORIENTATION_HORIZONTAL - - -def orientation_change_connect(handler, *args): - """ - @param handler(orientation, *args) -> None(?) - """ - initialScreenOrientation = get_screen_orientation() - orientationAndArgs = list(itertools.chain((initialScreenOrientation, ), args)) - - def _on_screen_size_changed(screen): - newScreenOrientation = get_screen_orientation() - if newScreenOrientation != orientationAndArgs[0]: - orientationAndArgs[0] = newScreenOrientation - handler(*orientationAndArgs) - - rootScreen = gtk.gdk.get_default_root_window() - return gtk.connect(rootScreen, "size-changed", _on_screen_size_changed) - - -@contextlib.contextmanager -def flock(path, timeout=-1): - WAIT_FOREVER = -1 - DELAY = 0.1 - timeSpent = 0 - - acquired = False - - while timeSpent <= timeout or timeout == WAIT_FOREVER: - try: - fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR) - acquired = True - break - except OSError, e: - if e.errno != errno.EEXIST: - raise - time.sleep(DELAY) - timeSpent += DELAY - - assert acquired, "Failed to grab file-lock %s within timeout %d" % (path, timeout) - - try: - yield fd - finally: - os.unlink(path) - - -@contextlib.contextmanager -def gtk_lock(): - gtk.gdk.threads_enter() - try: - yield - finally: - gtk.gdk.threads_leave() - - -def find_parent_window(widget): - while True: - parent = widget.get_parent() - if isinstance(parent, gtk.Window): - return parent - widget = parent - - -def make_idler(func): - """ - Decorator that makes a generator-function into a function that will continue execution on next call - """ - a = [] - - @functools.wraps(func) - def decorated_func(*args, **kwds): - if not a: - a.append(func(*args, **kwds)) - try: - a[0].next() - return True - except StopIteration: - del a[:] - return False - - return decorated_func - - -def asynchronous_gtk_message(original_func): - """ - @note Idea came from http://www.aclevername.com/articles/python-webgui/ - """ - - def execute(allArgs): - args, kwargs = allArgs - with gtk_lock(): - original_func(*args, **kwargs) - return False - - @functools.wraps(original_func) - def delayed_func(*args, **kwargs): - gobject.idle_add(execute, (args, kwargs)) - - return delayed_func - - -def synchronous_gtk_message(original_func): - """ - @note Idea came from http://www.aclevername.com/articles/python-webgui/ - """ - - @functools.wraps(original_func) - def immediate_func(*args, **kwargs): - with gtk_lock(): - return original_func(*args, **kwargs) - - return immediate_func - - -def autostart(func): - """ - >>> @autostart - ... def grep_sink(pattern): - ... print "Looking for %s" % pattern - ... while True: - ... line = yield - ... if pattern in line: - ... print line, - >>> g = grep_sink("python") - Looking for python - >>> g.send("Yeah but no but yeah but no") - >>> g.send("A series of tubes") - >>> g.send("python generators rock!") - python generators rock! - >>> g.close() - """ - - @functools.wraps(func) - def start(*args, **kwargs): - cr = func(*args, **kwargs) - cr.next() - return cr - - return start - - -@autostart -def printer_sink(format = "%s"): - """ - >>> pr = printer_sink("%r") - >>> pr.send("Hello") - 'Hello' - >>> pr.send("5") - '5' - >>> pr.send(5) - 5 - >>> p = printer_sink() - >>> p.send("Hello") - Hello - >>> p.send("World") - World - >>> # p.throw(RuntimeError, "Goodbye") - >>> # p.send("Meh") - >>> # p.close() - """ - while True: - item = yield - print format % (item, ) - - -@autostart -def null_sink(): - """ - Good for uses like with cochain to pick up any slack - """ - while True: - item = yield - - -@autostart -def comap(function, target): - """ - >>> p = printer_sink() - >>> cm = comap(lambda x: x+1, p) - >>> cm.send((0, )) - 1 - >>> cm.send((1.0, )) - 2.0 - >>> cm.send((-2, )) - -1 - """ - while True: - try: - item = yield - mappedItem = function(*item) - target.send(mappedItem) - except Exception, e: - _moduleLogger.exception("Forwarding exception!") - target.throw(e.__class__, str(e)) - - -def _flush_queue(queue): - while not queue.empty(): - yield queue.get() - - -@autostart -def queue_sink(queue): - """ - >>> q = Queue.Queue() - >>> qs = queue_sink(q) - >>> qs.send("Hello") - >>> qs.send("World") - >>> qs.throw(RuntimeError, "Goodbye") - >>> qs.send("Meh") - >>> qs.close() - >>> print [i for i in _flush_queue(q)] - [(None, 'Hello'), (None, 'World'), (, 'Goodbye'), (None, 'Meh'), (, None)] - """ - while True: - try: - item = yield - queue.put((None, item)) - except Exception, e: - queue.put((e.__class__, str(e))) - except GeneratorExit: - queue.put((GeneratorExit, None)) - raise - - -def decode_item(item, target): - if item[0] is None: - target.send(item[1]) - return False - elif item[0] is GeneratorExit: - target.close() - return True - else: - target.throw(item[0], item[1]) - return False - - -def nonqueue_source(queue, target): - isDone = False - while not isDone: - item = queue.get() - isDone = decode_item(item, target) - while not queue.empty(): - queue.get_nowait() - - -def threaded_stage(target, thread_factory = threading.Thread): - messages = Queue.Queue() - - run_source = functools.partial(nonqueue_source, messages, target) - thread = thread_factory(target=run_source) - thread.setDaemon(True) - thread.start() - - # Sink running in current thread - return queue_sink(messages) - - -def log_exception(logger): - - def log_exception_decorator(func): - - @functools.wraps(func) - def wrapper(*args, **kwds): - try: - return func(*args, **kwds) - except Exception: - logger.exception(func.__name__) - - return wrapper - - return log_exception_decorator - - -class LoginWindow(object): - - def __init__(self, widgetTree): - """ - @note Thread agnostic - """ - self._dialog = widgetTree.get_widget("loginDialog") - self._parentWindow = widgetTree.get_widget("mainWindow") - self._serviceCombo = widgetTree.get_widget("serviceCombo") - self._usernameEntry = widgetTree.get_widget("usernameentry") - self._passwordEntry = widgetTree.get_widget("passwordentry") - - self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING) - self._serviceCombo.set_model(self._serviceList) - cell = gtk.CellRendererText() - self._serviceCombo.pack_start(cell, True) - self._serviceCombo.add_attribute(cell, 'text', 1) - self._serviceCombo.set_active(0) - - widgetTree.get_widget("loginbutton").connect("clicked", self._on_loginbutton_clicked) - widgetTree.get_widget("logins_close_button").connect("clicked", self._on_loginclose_clicked) - - def request_credentials(self, - parentWindow = None, - defaultCredentials = ("", "") - ): - """ - @note UI Thread - """ - if parentWindow is None: - parentWindow = self._parentWindow - - self._serviceCombo.hide() - self._serviceList.clear() - - self._usernameEntry.set_text(defaultCredentials[0]) - self._passwordEntry.set_text(defaultCredentials[1]) - - try: - self._dialog.set_transient_for(parentWindow) - self._dialog.set_default_response(gtk.RESPONSE_OK) - response = self._dialog.run() - if response != gtk.RESPONSE_OK: - raise RuntimeError("Login Cancelled") - - username = self._usernameEntry.get_text() - password = self._passwordEntry.get_text() - self._passwordEntry.set_text("") - finally: - self._dialog.hide() - - return username, password - - def request_credentials_from(self, - services, - parentWindow = None, - defaultCredentials = ("", "") - ): - """ - @note UI Thread - """ - if parentWindow is None: - parentWindow = self._parentWindow - - self._serviceList.clear() - for serviceIdserviceName in services: - self._serviceList.append(serviceIdserviceName) - self._serviceCombo.set_active(0) - self._serviceCombo.show() - - self._usernameEntry.set_text(defaultCredentials[0]) - self._passwordEntry.set_text(defaultCredentials[1]) - - try: - self._dialog.set_transient_for(parentWindow) - self._dialog.set_default_response(gtk.RESPONSE_OK) - response = self._dialog.run() - if response != gtk.RESPONSE_OK: - raise RuntimeError("Login Cancelled") - - username = self._usernameEntry.get_text() - password = self._passwordEntry.get_text() - finally: - self._dialog.hide() - - itr = self._serviceCombo.get_active_iter() - serviceId = int(self._serviceList.get_value(itr, 0)) - self._serviceList.clear() - return serviceId, username, password - - def _on_loginbutton_clicked(self, *args): - self._dialog.response(gtk.RESPONSE_OK) - - def _on_loginclose_clicked(self, *args): - self._dialog.response(gtk.RESPONSE_CANCEL) - - -def safecall(f, errorDisplay=None, default=None, exception=Exception): - ''' - Returns modified f. When the modified f is called and throws an - exception, the default value is returned - ''' - def _safecall(*args, **argv): - try: - return f(*args,**argv) - except exception, e: - if errorDisplay is not None: - errorDisplay.push_exception(e) - return default - return _safecall - - -class ErrorDisplay(object): - - def __init__(self, errorBox, errorDescription, errorClose): - super(ErrorDisplay, self).__init__() - self.__errorBox = errorBox - self.__errorDescription = errorDescription - self.__errorClose = errorClose - self.__parentBox = self.__errorBox.get_parent() - - self.__errorBox.connect("button_release_event", self._on_close) - - self.__messages = [] - self.__parentBox.remove(self.__errorBox) - - def push_message_with_lock(self, message): - with gtk_lock(): - self.push_message(message) - - def push_message(self, message): - self.__messages.append(message) - if 1 == len(self.__messages): - self.__show_message(message) - - def push_exception_with_lock(self): - with gtk_lock(): - self.push_exception() - - def push_exception(self): - userMessage = str(sys.exc_info()[1]) - self.push_message(userMessage) - _moduleLogger.exception(userMessage) - - def pop_message(self): - del self.__messages[0] - if 0 == len(self.__messages): - self.__hide_message() - else: - self.__errorDescription.set_text(self.__messages[0]) - - def _on_close(self, *args): - self.pop_message() - - def __show_message(self, message): - self.__errorDescription.set_text(message) - self.__parentBox.pack_start(self.__errorBox, False, False) - self.__parentBox.reorder_child(self.__errorBox, 1) - - def __hide_message(self): - self.__errorDescription.set_text("") - self.__parentBox.remove(self.__errorBox) - - -class DummyErrorDisplay(object): - - def __init__(self): - super(DummyErrorDisplay, self).__init__() - - self.__messages = [] - - def push_message_with_lock(self, message): - self.push_message(message) - - def push_message(self, message): - if 0 < len(self.__messages): - self.__messages.append(message) - else: - self.__show_message(message) - - def push_exception(self, exception = None): - userMessage = str(sys.exc_value) - _moduleLogger.exception(userMessage) - - def pop_message(self): - if 0 < len(self.__messages): - self.__show_message(self.__messages[0]) - del self.__messages[0] - - def __show_message(self, message): - _moduleLogger.debug(message) - - -class MessageBox(gtk.MessageDialog): - - def __init__(self, message): - parent = None - gtk.MessageDialog.__init__( - self, - parent, - gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, - gtk.MESSAGE_ERROR, - gtk.BUTTONS_OK, - message, - ) - self.set_default_response(gtk.RESPONSE_OK) - self.connect('response', self._handle_clicked) - - def _handle_clicked(self, *args): - self.destroy() - - -class MessageBox2(gtk.MessageDialog): - - def __init__(self, message): - parent = None - gtk.MessageDialog.__init__( - self, - parent, - gtk.DIALOG_DESTROY_WITH_PARENT, - gtk.MESSAGE_ERROR, - gtk.BUTTONS_OK, - message, - ) - self.set_default_response(gtk.RESPONSE_OK) - self.connect('response', self._handle_clicked) - - def _handle_clicked(self, *args): - self.destroy() - - -class PopupCalendar(object): - - def __init__(self, parent, displayDate, title = ""): - self._displayDate = displayDate - - self._calendar = gtk.Calendar() - self._calendar.select_month(self._displayDate.month, self._displayDate.year) - self._calendar.select_day(self._displayDate.day) - self._calendar.set_display_options( - gtk.CALENDAR_SHOW_HEADING | - gtk.CALENDAR_SHOW_DAY_NAMES | - gtk.CALENDAR_NO_MONTH_CHANGE | - 0 - ) - self._calendar.connect("day-selected", self._on_day_selected) - - self._popupWindow = gtk.Window() - self._popupWindow.set_title(title) - self._popupWindow.add(self._calendar) - self._popupWindow.set_transient_for(parent) - self._popupWindow.set_modal(True) - self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) - self._popupWindow.set_skip_pager_hint(True) - self._popupWindow.set_skip_taskbar_hint(True) - - def run(self): - self._popupWindow.show_all() - - def _on_day_selected(self, *args): - try: - self._calendar.select_month(self._displayDate.month, self._displayDate.year) - self._calendar.select_day(self._displayDate.day) - except Exception, e: - _moduleLogger.exception(e) - - -class QuickAddView(object): - - def __init__(self, widgetTree, errorDisplay, signalSink, prefix): - self._errorDisplay = errorDisplay - self._manager = None - self._signalSink = signalSink - - self._clipboard = gtk.clipboard_get() - - self._taskNameEntry = widgetTree.get_widget(prefix+"-nameEntry") - self._addTaskButton = widgetTree.get_widget(prefix+"-addButton") - self._pasteTaskNameButton = widgetTree.get_widget(prefix+"-pasteNameButton") - self._clearTaskNameButton = widgetTree.get_widget(prefix+"-clearNameButton") - self._onAddId = None - self._onAddClickedId = None - self._onAddReleasedId = None - self._addToEditTimerId = None - self._onClearId = None - self._onPasteId = None - - def enable(self, manager): - self._manager = manager - - self._onAddId = self._addTaskButton.connect("clicked", self._on_add) - self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed) - self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released) - self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste) - self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear) - - def disable(self): - self._manager = None - - self._addTaskButton.disconnect(self._onAddId) - self._addTaskButton.disconnect(self._onAddClickedId) - self._addTaskButton.disconnect(self._onAddReleasedId) - self._pasteTaskNameButton.disconnect(self._onPasteId) - self._clearTaskNameButton.disconnect(self._onClearId) - - def set_addability(self, addability): - self._addTaskButton.set_sensitive(addability) - - def _on_add(self, *args): - try: - name = self._taskNameEntry.get_text() - self._taskNameEntry.set_text("") - - self._signalSink.stage.send(("add", name)) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_add_edit(self, *args): - try: - name = self._taskNameEntry.get_text() - self._taskNameEntry.set_text("") - - self._signalSink.stage.send(("add-edit", name)) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_add_pressed(self, widget): - try: - self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_add_released(self, widget): - try: - if self._addToEditTimerId is not None: - gobject.source_remove(self._addToEditTimerId) - self._addToEditTimerId = None - except Exception, e: - self._errorDisplay.push_exception() - - def _on_paste(self, *args): - try: - entry = self._taskNameEntry.get_text() - addedText = self._clipboard.wait_for_text() - if addedText: - entry += addedText - self._taskNameEntry.set_text(entry) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_clear(self, *args): - try: - self._taskNameEntry.set_text("") - except Exception, e: - self._errorDisplay.push_exception() - - -class TapOrHold(object): - - def __init__(self, widget): - self._widget = widget - self._isTap = True - self._isPointerInside = True - self._holdTimeoutId = None - self._tapTimeoutId = None - self._taps = 0 - - self._bpeId = None - self._breId = None - self._eneId = None - self._lneId = None - - def enable(self): - self._bpeId = self._widget.connect("button-press-event", self._on_button_press) - self._breId = self._widget.connect("button-release-event", self._on_button_release) - self._eneId = self._widget.connect("enter-notify-event", self._on_enter) - self._lneId = self._widget.connect("leave-notify-event", self._on_leave) - - def disable(self): - self._widget.disconnect(self._bpeId) - self._widget.disconnect(self._breId) - self._widget.disconnect(self._eneId) - self._widget.disconnect(self._lneId) - - def on_tap(self, taps): - print "TAP", taps - - def on_hold(self, taps): - print "HOLD", taps - - def on_holding(self): - print "HOLDING" - - def on_cancel(self): - print "CANCEL" - - def _on_button_press(self, *args): - # Hack to handle weird notebook behavior - self._isPointerInside = True - self._isTap = True - - if self._tapTimeoutId is not None: - gobject.source_remove(self._tapTimeoutId) - self._tapTimeoutId = None - - # Handle double taps - if self._holdTimeoutId is None: - self._tapTimeoutId = None - - self._taps = 1 - self._holdTimeoutId = gobject.timeout_add(1000, self._on_hold_timeout) - else: - self._taps = 2 - - def _on_button_release(self, *args): - assert self._tapTimeoutId is None - # Handle release after timeout if user hasn't double-clicked - self._tapTimeoutId = gobject.timeout_add(100, self._on_tap_timeout) - - def _on_actual_press(self, *args): - if self._holdTimeoutId is not None: - gobject.source_remove(self._holdTimeoutId) - self._holdTimeoutId = None - - if self._isPointerInside: - if self._isTap: - self.on_tap(self._taps) - else: - self.on_hold(self._taps) - else: - self.on_cancel() - - def _on_tap_timeout(self, *args): - self._tapTimeoutId = None - self._on_actual_press() - return False - - def _on_hold_timeout(self, *args): - self._holdTimeoutId = None - self._isTap = False - self.on_holding() - return False - - def _on_enter(self, *args): - self._isPointerInside = True - - def _on_leave(self, *args): - self._isPointerInside = False - - -if __name__ == "__main__": - if False: - import datetime - cal = PopupCalendar(None, datetime.datetime.now()) - cal._popupWindow.connect("destroy", lambda w: gtk.main_quit()) - cal.run() - - gtk.main() diff --git a/src/gv_views.py b/src/gv_views.py deleted file mode 100644 index f9e5349..0000000 --- a/src/gv_views.py +++ /dev/null @@ -1,1827 +0,0 @@ -#!/usr/bin/env python - -""" -DialCentral - Front end for Google's GoogleVoice service. -Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -@todo Collapse voicemails -""" - -from __future__ import with_statement - -import re -import ConfigParser -import itertools -import logging - -import gobject -import pango -import gtk - -import gtk_toolbox -import hildonize -from backends import gv_backend -from backends import null_backend - - -_moduleLogger = logging.getLogger("gv_views") - - -def make_ugly(prettynumber): - """ - function to take a phone number and strip out all non-numeric - characters - - >>> make_ugly("+012-(345)-678-90") - '+01234567890' - """ - return normalize_number(prettynumber) - - -def normalize_number(prettynumber): - """ - function to take a phone number and strip out all non-numeric - characters - - >>> normalize_number("+012-(345)-678-90") - '+01234567890' - >>> normalize_number("1-(345)-678-9000") - '+13456789000' - >>> normalize_number("+1-(345)-678-9000") - '+13456789000' - """ - uglynumber = re.sub('[^0-9+]', '', prettynumber) - - if uglynumber.startswith("+"): - pass - elif uglynumber.startswith("1"): - uglynumber = "+"+uglynumber - elif 10 <= len(uglynumber): - assert uglynumber[0] not in ("+", "1") - uglynumber = "+1"+uglynumber - else: - pass - - return uglynumber - - -def _make_pretty_with_areacode(phonenumber): - prettynumber = "(%s)" % (phonenumber[0:3], ) - if 3 < len(phonenumber): - prettynumber += " %s" % (phonenumber[3:6], ) - if 6 < len(phonenumber): - prettynumber += "-%s" % (phonenumber[6:], ) - return prettynumber - - -def _make_pretty_local(phonenumber): - prettynumber = "%s" % (phonenumber[0:3], ) - if 3 < len(phonenumber): - prettynumber += "-%s" % (phonenumber[3:], ) - return prettynumber - - -def _make_pretty_international(phonenumber): - prettynumber = phonenumber - if phonenumber.startswith("1"): - prettynumber = "1 " - prettynumber += _make_pretty_with_areacode(phonenumber[1:]) - return prettynumber - - -def make_pretty(phonenumber): - """ - Function to take a phone number and return the pretty version - pretty numbers: - if phonenumber begins with 0: - ...-(...)-...-.... - if phonenumber begins with 1: ( for gizmo callback numbers ) - 1 (...)-...-.... - if phonenumber is 13 digits: - (...)-...-.... - if phonenumber is 10 digits: - ...-.... - >>> make_pretty("12") - '12' - >>> make_pretty("1234567") - '123-4567' - >>> make_pretty("2345678901") - '+1 (234) 567-8901' - >>> make_pretty("12345678901") - '+1 (234) 567-8901' - >>> make_pretty("01234567890") - '+012 (345) 678-90' - >>> make_pretty("+01234567890") - '+012 (345) 678-90' - >>> make_pretty("+12") - '+1 (2)' - >>> make_pretty("+123") - '+1 (23)' - >>> make_pretty("+1234") - '+1 (234)' - """ - if phonenumber is None or phonenumber is "": - return "" - - phonenumber = normalize_number(phonenumber) - - if phonenumber[0] == "+": - prettynumber = _make_pretty_international(phonenumber[1:]) - if not prettynumber.startswith("+"): - prettynumber = "+"+prettynumber - elif 8 < len(phonenumber) and phonenumber[0] in ("1", ): - prettynumber = _make_pretty_international(phonenumber) - elif 7 < len(phonenumber): - prettynumber = _make_pretty_with_areacode(phonenumber) - elif 3 < len(phonenumber): - prettynumber = _make_pretty_local(phonenumber) - else: - prettynumber = phonenumber - return prettynumber.strip() - - -def abbrev_relative_date(date): - """ - >>> abbrev_relative_date("42 hours ago") - '42 h' - >>> abbrev_relative_date("2 days ago") - '2 d' - >>> abbrev_relative_date("4 weeks ago") - '4 w' - """ - parts = date.split(" ") - return "%s %s" % (parts[0], parts[1][0]) - - -def _collapse_message(messageLines, maxCharsPerLine, maxLines): - lines = 0 - - numLines = len(messageLines) - for line in messageLines[0:min(maxLines, numLines)]: - linesPerLine = max(1, int(len(line) / maxCharsPerLine)) - allowedLines = maxLines - lines - acceptedLines = min(allowedLines, linesPerLine) - acceptedChars = acceptedLines * maxCharsPerLine - - if acceptedChars < (len(line) + 3): - suffix = "..." - else: - acceptedChars = len(line) # eh, might as well complete the line - suffix = "" - abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix) - yield abbrevMessage - - lines += acceptedLines - if maxLines <= lines: - break - - -def collapse_message(message, maxCharsPerLine, maxLines): - r""" - >>> collapse_message("Hello", 60, 2) - 'Hello' - >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2) - 'Hello world how are you doing today? 01234567890123456789012...' - >>> collapse_message('''Hello world how are you doing today? - ... 01234567890123456789 - ... 01234567890123456789 - ... 01234567890123456789 - ... 01234567890123456789''', 60, 2) - 'Hello world how are you doing today?\n01234567890123456789' - >>> collapse_message(''' - ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789 - ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789 - ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789 - ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789 - ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789 - ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2) - '\nHello world how are you doing today? 01234567890123456789012...' - """ - messageLines = message.split("\n") - return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines)) - - -def _get_contact_numbers(backend, contactId, number): - if contactId and contactId != '0': - contactPhoneNumbers = list(backend.get_contact_details(contactId)) - uglyContactNumbers = ( - make_ugly(contactNumber) - for (numberDescription, contactNumber) in contactPhoneNumbers - ) - defaultMatches = [ - ( - number == contactNumber or - number[1:] == contactNumber and number.startswith("1") or - number[2:] == contactNumber and number.startswith("+1") or - number == contactNumber[1:] and contactNumber.startswith("1") or - number == contactNumber[2:] and contactNumber.startswith("+1") - ) - for contactNumber in uglyContactNumbers - ] - try: - defaultIndex = defaultMatches.index(True) - except ValueError: - contactPhoneNumbers.append(("Other", number)) - defaultIndex = len(contactPhoneNumbers)-1 - _moduleLogger.warn( - "Could not find contact %r's number %s among %r" % ( - contactId, number, contactPhoneNumbers - ) - ) - else: - contactPhoneNumbers = [("Phone", number)] - defaultIndex = -1 - - return contactPhoneNumbers, defaultIndex - - -class SmsEntryWindow(object): - - MAX_CHAR = 160 - - def __init__(self, widgetTree, parent, app): - self._clipboard = gtk.clipboard_get() - self._widgetTree = widgetTree - self._parent = parent - self._app = app - self._isFullScreen = False - - self._window = self._widgetTree.get_widget("smsWindow") - self._window = hildonize.hildonize_window(self._app, self._window) - self._window.set_title("SMS") - self._window.connect("delete-event", self._on_delete) - self._window.connect("key-press-event", self._on_key_press) - self._window.connect("window-state-event", self._on_window_state_change) - self._widgetTree.get_widget("smsMessagesViewPort").get_parent().show() - - errorBox = self._widgetTree.get_widget("smsErrorEventBox") - errorDescription = self._widgetTree.get_widget("smsErrorDescription") - errorClose = self._widgetTree.get_widget("smsErrorClose") - self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose) - - self._smsButton = self._widgetTree.get_widget("sendSmsButton") - self._smsButton.connect("clicked", self._on_send) - self._dialButton = self._widgetTree.get_widget("dialButton") - self._dialButton.connect("clicked", self._on_dial) - - self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount") - - self._messagemodel = gtk.ListStore(gobject.TYPE_STRING) - self._messagesView = self._widgetTree.get_widget("smsMessages") - - textrenderer = gtk.CellRendererText() - textrenderer.set_property("wrap-mode", pango.WRAP_WORD) - textrenderer.set_property("wrap-width", 450) - messageColumn = gtk.TreeViewColumn("") - messageColumn.pack_start(textrenderer, expand=True) - messageColumn.add_attribute(textrenderer, "markup", 0) - messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - self._messagesView.append_column(messageColumn) - self._messagesView.set_headers_visible(False) - self._messagesView.set_model(self._messagemodel) - self._messagesView.set_fixed_height_mode(False) - - self._conversationView = self._messagesView.get_parent() - self._conversationViewPort = self._conversationView.get_parent() - self._scrollWindow = self._conversationViewPort.get_parent() - - self._targetList = self._widgetTree.get_widget("smsTargetList") - self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection") - self._phoneButton.connect("clicked", self._on_phone) - self._smsEntry = self._widgetTree.get_widget("smsEntry") - self._smsEntry.get_buffer().connect("changed", self._on_entry_changed) - self._smsEntrySize = None - - self._contacts = [] - - def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1): - contactNumbers = list(self._to_contact_numbers(contactDetails)) - assert contactNumbers, "Contact must have at least one number" - contactIndex = defaultIndex if defaultIndex != -1 else 0 - contact = contactNumbers, contactIndex, messages - self._contacts.append(contact) - - nameLabel = gtk.Label(name) - selector = gtk.Button(contactNumbers[0][1]) - if len(contactNumbers) == 1: - selector.set_sensitive(False) - removeContact = gtk.Button(stock="gtk-delete") - row = gtk.HBox() - row.pack_start(nameLabel, True, True) - row.pack_start(selector, True, True) - row.pack_start(removeContact, False, False) - row.show_all() - self._targetList.pack_start(row) - selector.connect("clicked", self._on_choose_phone_n, row) - removeContact.connect("clicked", self._on_remove_phone_n, row) - self._update_button_state() - self._update_context() - - parentSize = self._parent.get_size() - self._window.resize(parentSize[0], max(parentSize[1]-10, 100)) - self._window.show() - self._window.present() - - self._smsEntry.grab_focus() - self._scroll_to_bottom() - - def clear(self): - del self._contacts[:] - - for row in list(self._targetList.get_children()): - self._targetList.remove(row) - self._smsEntry.get_buffer().set_text("") - self._update_letter_count() - self._update_context() - - def fullscreen(self): - self._window.fullscreen() - - def unfullscreen(self): - self._window.unfullscreen() - - def _remove_contact(self, contactIndex): - del self._contacts[contactIndex] - - row = list(self._targetList.get_children())[contactIndex] - self._targetList.remove(row) - self._update_button_state() - self._update_context() - self._scroll_to_bottom() - - def _scroll_to_bottom(self): - dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height - dx = max(dx, 0) - adjustment = self._scrollWindow.get_vadjustment() - adjustment.value = dx - - def _update_letter_count(self): - if self._smsEntrySize is None: - self._smsEntrySize = self._smsEntry.size_request() - else: - self._smsEntry.set_size_request(*self._smsEntrySize) - entryLength = self._smsEntry.get_buffer().get_char_count() - - numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR) - if numTexts: - self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText)) - else: - self._letterCountLabel.set_text("%s" % (numCharInText, )) - - self._update_button_state() - - def _update_context(self): - self._messagemodel.clear() - if len(self._contacts) == 0: - self._messagesView.hide() - self._targetList.hide() - self._phoneButton.hide() - self._phoneButton.set_label("Error: You shouldn't see this") - elif len(self._contacts) == 1: - contactNumbers, index, messages = self._contacts[0] - if messages: - self._messagesView.show() - for message in messages: - row = (message, ) - self._messagemodel.append(row) - messagesSelection = self._messagesView.get_selection() - messagesSelection.select_path((len(messages)-1, )) - else: - self._messagesView.hide() - self._targetList.hide() - self._phoneButton.show() - self._phoneButton.set_label(contactNumbers[index][1]) - if 1 < len(contactNumbers): - self._phoneButton.set_sensitive(True) - else: - self._phoneButton.set_sensitive(False) - else: - self._messagesView.hide() - self._targetList.show() - self._phoneButton.hide() - self._phoneButton.set_label("Error: You shouldn't see this") - - def _update_button_state(self): - if len(self._contacts) == 0: - self._dialButton.set_sensitive(False) - self._smsButton.set_sensitive(False) - elif len(self._contacts) == 1: - entryLength = self._smsEntry.get_buffer().get_char_count() - if entryLength == 0: - self._dialButton.set_sensitive(True) - self._smsButton.set_sensitive(False) - else: - self._dialButton.set_sensitive(False) - self._smsButton.set_sensitive(True) - else: - self._dialButton.set_sensitive(False) - self._smsButton.set_sensitive(True) - - def _to_contact_numbers(self, contactDetails): - for phoneType, phoneNumber in contactDetails: - display = " - ".join((make_pretty(phoneNumber), phoneType)) - yield (phoneNumber, display) - - def _pseudo_destroy(self): - self.clear() - self._window.hide() - - def _request_number(self, contactIndex): - contactNumbers, index, messages = self._contacts[contactIndex] - assert 0 <= index, "%r" % index - - index = hildonize.touch_selector( - self._window, - "Phone Numbers", - (description for (number, description) in contactNumbers), - index, - ) - self._contacts[contactIndex] = contactNumbers, index, messages - - def send_sms(self, numbers, message): - raise NotImplementedError() - - def dial(self, number): - raise NotImplementedError() - - def _on_phone(self, *args): - try: - assert len(self._contacts) == 1, "One and only one contact is required" - self._request_number(0) - - contactNumbers, numberIndex, messages = self._contacts[0] - self._phoneButton.set_label(contactNumbers[numberIndex][1]) - row = list(self._targetList.get_children())[0] - phoneButton = list(row.get_children())[1] - phoneButton.set_label(contactNumbers[numberIndex][1]) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_choose_phone_n(self, button, row): - try: - assert 1 < len(self._contacts), "More than one contact required" - targetList = list(self._targetList.get_children()) - index = targetList.index(row) - self._request_number(index) - - contactNumbers, numberIndex, messages = self._contacts[0] - phoneButton = list(row.get_children())[1] - phoneButton.set_label(contactNumbers[numberIndex][1]) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_remove_phone_n(self, button, row): - try: - assert 1 < len(self._contacts), "More than one contact required" - targetList = list(self._targetList.get_children()) - index = targetList.index(row) - - del self._contacts[index] - self._targetList.remove(row) - self._update_context() - self._update_button_state() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_entry_changed(self, *args): - try: - self._update_letter_count() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_send(self, *args): - try: - assert 0 < len(self._contacts), "At least one contact required (%r)" % self._contacts - phoneNumbers = [ - make_ugly(contact[0][contact[1]][0]) - for contact in self._contacts - ] - - entryBuffer = self._smsEntry.get_buffer() - enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter()) - enteredMessage = enteredMessage.strip() - assert enteredMessage, "No message provided" - self.send_sms(phoneNumbers, enteredMessage) - self._pseudo_destroy() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_dial(self, *args): - try: - assert len(self._contacts) == 1, "One and only one contact allowed (%r)" % self._contacts - contact = self._contacts[0] - contactNumber = contact[0][contact[1]][0] - phoneNumber = make_ugly(contactNumber) - self.dial(phoneNumber) - self._pseudo_destroy() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_delete(self, *args): - try: - self._window.emit_stop_by_name("delete-event") - if hildonize.IS_FREMANTLE_SUPPORTED: - self._window.hide() - else: - self._pseudo_destroy() - except Exception, e: - self._errorDisplay.push_exception() - return True - - def _on_window_state_change(self, widget, event, *args): - try: - if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: - self._isFullScreen = True - else: - self._isFullScreen = False - except Exception, e: - self._errorDisplay.push_exception() - - def _on_key_press(self, widget, event): - RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter) - try: - if ( - event.keyval == gtk.keysyms.F6 or - event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK - ): - if self._isFullScreen: - self._window.unfullscreen() - else: - self._window.fullscreen() - elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK: - message = "\n".join( - messagePart[0] - for messagePart in self._messagemodel - ) - self._clipboard.set_text(str(message)) - elif ( - event.keyval == gtk.keysyms.h and - event.get_state() & gtk.gdk.CONTROL_MASK - ): - self._window.hide() - elif ( - event.keyval == gtk.keysyms.w and - event.get_state() & gtk.gdk.CONTROL_MASK - ): - self._pseudo_destroy() - elif ( - event.keyval == gtk.keysyms.q and - event.get_state() & gtk.gdk.CONTROL_MASK - ): - self._parent.destroy() - except Exception, e: - self._errorDisplay.push_exception() - - -class Dialpad(object): - - def __init__(self, widgetTree, errorDisplay): - self._clipboard = gtk.clipboard_get() - self._errorDisplay = errorDisplay - - self._numberdisplay = widgetTree.get_widget("numberdisplay") - self._callButton = widgetTree.get_widget("dialpadCall") - self._sendSMSButton = widgetTree.get_widget("dialpadSMS") - self._backButton = widgetTree.get_widget("back") - self._plusButton = widgetTree.get_widget("plus") - self._phonenumber = "" - self._prettynumber = "" - - callbackMapping = { - "on_digit_clicked": self._on_digit_clicked, - } - widgetTree.signal_autoconnect(callbackMapping) - self._sendSMSButton.connect("clicked", self._on_sms_clicked) - self._callButton.connect("clicked", self._on_call_clicked) - self._plusButton.connect("clicked", self._on_plus) - - self._originalLabel = self._backButton.get_label() - self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton) - self._backTapHandler.on_tap = self._on_backspace - self._backTapHandler.on_hold = self._on_clearall - self._backTapHandler.on_holding = self._set_clear_button - self._backTapHandler.on_cancel = self._reset_back_button - - self._window = gtk_toolbox.find_parent_window(self._numberdisplay) - self._keyPressEventId = 0 - - def enable(self): - self._sendSMSButton.grab_focus() - self._backTapHandler.enable() - self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press) - - def disable(self): - self._window.disconnect(self._keyPressEventId) - self._keyPressEventId = 0 - self._reset_back_button() - self._backTapHandler.disable() - - def add_contact(self, *args, **kwds): - """ - @note Actual function is patched in later - """ - raise NotImplementedError("Horrible unknown error has occurred") - - def dial(self, number): - """ - @note Actual function is patched in later - """ - raise NotImplementedError("Horrible unknown error has occurred") - - def get_number(self): - return self._phonenumber - - def set_number(self, number): - """ - Set the number to dial - """ - try: - self._phonenumber = make_ugly(number) - self._prettynumber = make_pretty(self._phonenumber) - self._numberdisplay.set_label("%s" % (self._prettynumber)) - if self._phonenumber: - self._plusButton.set_sensitive(False) - else: - self._plusButton.set_sensitive(True) - except TypeError, e: - self._errorDisplay.push_exception() - - def clear(self): - self.set_number("") - - @staticmethod - def name(): - return "Dialpad" - - def load_settings(self, config, section): - pass - - def save_settings(self, config, section): - """ - @note Thread Agnostic - """ - pass - - def set_orientation(self, orientation): - if orientation == gtk.ORIENTATION_VERTICAL: - pass - elif orientation == gtk.ORIENTATION_HORIZONTAL: - pass - else: - raise NotImplementedError(orientation) - - def _on_key_press(self, widget, event): - try: - if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK: - contents = self._clipboard.wait_for_text() - if contents is not None: - self.set_number(contents) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_call_clicked(self, widget): - try: - phoneNumber = self.get_number() - self.dial(phoneNumber) - self.set_number("") - except Exception, e: - self._errorDisplay.push_exception() - - def _on_sms_clicked(self, widget): - try: - phoneNumber = self.get_number() - self.add_contact( - "(Dialpad)", - [("Dialer", phoneNumber)], () - ) - self.set_number("") - except Exception, e: - self._errorDisplay.push_exception() - - def _on_digit_clicked(self, widget): - try: - self.set_number(self._phonenumber + widget.get_name()[-1]) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_plus(self, *args): - try: - self.set_number(self._phonenumber + "+") - except Exception, e: - self._errorDisplay.push_exception() - - def _on_backspace(self, taps): - try: - self.set_number(self._phonenumber[:-taps]) - self._reset_back_button() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_clearall(self, taps): - try: - self.clear() - self._reset_back_button() - except Exception, e: - self._errorDisplay.push_exception() - return False - - def _set_clear_button(self): - try: - self._backButton.set_label("gtk-clear") - except Exception, e: - self._errorDisplay.push_exception() - - def _reset_back_button(self): - try: - self._backButton.set_label(self._originalLabel) - except Exception, e: - self._errorDisplay.push_exception() - - -class AccountInfo(object): - - def __init__(self, widgetTree, backend, alarmHandler, errorDisplay): - self._errorDisplay = errorDisplay - self._backend = backend - self._isPopulated = False - self._alarmHandler = alarmHandler - self._notifyOnMissed = False - self._notifyOnVoicemail = False - self._notifyOnSms = False - - self._callbackList = [] - self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display") - self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton") - self._onCallbackSelectChangedId = 0 - - self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox") - self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton") - self._missedCheckbox = widgetTree.get_widget("missedCheckbox") - self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox") - self._smsCheckbox = widgetTree.get_widget("smsCheckbox") - self._onNotifyToggled = 0 - self._onMinutesChanged = 0 - self._onMissedToggled = 0 - self._onVoicemailToggled = 0 - self._onSmsToggled = 0 - self._applyAlarmTimeoutId = None - - self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton) - self._callbackNumber = "" - - def enable(self): - assert self._backend.is_authed(), "Attempting to enable backend while not logged in" - - self._accountViewNumberDisplay.set_use_markup(True) - self.set_account_number("") - - del self._callbackList[:] - self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked) - self._set_callback_label("") - - if self._alarmHandler is not None: - self._notifyCheckbox.set_active(self._alarmHandler.isEnabled) - self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence) - self._missedCheckbox.set_active(self._notifyOnMissed) - self._voicemailCheckbox.set_active(self._notifyOnVoicemail) - self._smsCheckbox.set_active(self._notifyOnSms) - - self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled) - self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked) - self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled) - self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled) - self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled) - else: - self._notifyCheckbox.set_sensitive(False) - self._minutesEntryButton.set_sensitive(False) - self._missedCheckbox.set_sensitive(False) - self._voicemailCheckbox.set_sensitive(False) - self._smsCheckbox.set_sensitive(False) - - self.update(force=True) - - def disable(self): - self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId) - self._onCallbackSelectChangedId = 0 - self._set_callback_label("") - - if self._alarmHandler is not None: - self._notifyCheckbox.disconnect(self._onNotifyToggled) - self._minutesEntryButton.disconnect(self._onMinutesChanged) - self._missedCheckbox.disconnect(self._onNotifyToggled) - self._voicemailCheckbox.disconnect(self._onNotifyToggled) - self._smsCheckbox.disconnect(self._onNotifyToggled) - self._onNotifyToggled = 0 - self._onMinutesChanged = 0 - self._onMissedToggled = 0 - self._onVoicemailToggled = 0 - self._onSmsToggled = 0 - else: - self._notifyCheckbox.set_sensitive(True) - self._minutesEntryButton.set_sensitive(True) - self._missedCheckbox.set_sensitive(True) - self._voicemailCheckbox.set_sensitive(True) - self._smsCheckbox.set_sensitive(True) - - self.clear() - del self._callbackList[:] - - def set_account_number(self, number): - """ - Displays current account number - """ - self._accountViewNumberDisplay.set_label("%s" % (number)) - - def update(self, force = False): - if not force and self._isPopulated: - return False - self._populate_callback_combo() - self.set_account_number(self._backend.get_account_number()) - return True - - def clear(self): - self._set_callback_label("") - self.set_account_number("") - self._isPopulated = False - - def save_everything(self): - raise NotImplementedError - - @staticmethod - def name(): - return "Account Info" - - def load_settings(self, config, section): - self._callbackNumber = make_ugly(config.get(section, "callback")) - self._notifyOnMissed = config.getboolean(section, "notifyOnMissed") - self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail") - self._notifyOnSms = config.getboolean(section, "notifyOnSms") - - def save_settings(self, config, section): - """ - @note Thread Agnostic - """ - config.set(section, "callback", self._callbackNumber) - config.set(section, "notifyOnMissed", repr(self._notifyOnMissed)) - config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail)) - config.set(section, "notifyOnSms", repr(self._notifyOnSms)) - - def set_orientation(self, orientation): - if orientation == gtk.ORIENTATION_VERTICAL: - pass - elif orientation == gtk.ORIENTATION_HORIZONTAL: - pass - else: - raise NotImplementedError(orientation) - - def _populate_callback_combo(self): - self._isPopulated = True - del self._callbackList[:] - try: - callbackNumbers = self._backend.get_callback_numbers() - except Exception, e: - self._errorDisplay.push_exception() - self._isPopulated = False - return - - if len(callbackNumbers) == 0: - callbackNumbers = {"": "No callback numbers available"} - - for number, description in callbackNumbers.iteritems(): - self._callbackList.append((make_pretty(number), description)) - - self._set_callback_number(self._callbackNumber) - - def _set_callback_number(self, number): - try: - if not self._backend.is_valid_syntax(number) and 0 < len(number): - self._errorDisplay.push_message("%s is not a valid callback number" % number) - elif number == self._backend.get_callback_number() and 0 < len(number): - _moduleLogger.warning( - "Callback number already is %s" % ( - self._backend.get_callback_number(), - ), - ) - self._set_callback_label(number) - else: - if number.startswith("1747"): number = "+" + number - self._backend.set_callback_number(number) - assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % ( - make_pretty(number), make_pretty(self._backend.get_callback_number()) - ) - self._callbackNumber = make_ugly(number) - self._set_callback_label(number) - _moduleLogger.info( - "Callback number set to %s" % ( - self._backend.get_callback_number(), - ), - ) - except Exception, e: - self._errorDisplay.push_exception() - - def _set_callback_label(self, uglyNumber): - prettyNumber = make_pretty(uglyNumber) - if len(prettyNumber) == 0: - prettyNumber = "No Callback Number" - self._callbackSelectButton.set_label(prettyNumber) - - def _update_alarm_settings(self, recurrence): - try: - isEnabled = self._notifyCheckbox.get_active() - if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence: - self._alarmHandler.apply_settings(isEnabled, recurrence) - finally: - self.save_everything() - self._notifyCheckbox.set_active(self._alarmHandler.isEnabled) - self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence) - - def _on_callbackentry_clicked(self, *args): - try: - actualSelection = make_pretty(self._callbackNumber) - - userOptions = dict( - (number, "%s (%s)" % (number, description)) - for (number, description) in self._callbackList - ) - defaultSelection = userOptions.get(actualSelection, actualSelection) - - userSelection = hildonize.touch_selector_entry( - self._window, - "Callback Number", - list(userOptions.itervalues()), - defaultSelection, - ) - reversedUserOptions = dict( - itertools.izip(userOptions.itervalues(), userOptions.iterkeys()) - ) - selectedNumber = reversedUserOptions.get(userSelection, userSelection) - - number = make_ugly(selectedNumber) - self._set_callback_number(number) - except RuntimeError, e: - _moduleLogger.exception("%s" % str(e)) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_notify_toggled(self, *args): - try: - if self._applyAlarmTimeoutId is not None: - gobject.source_remove(self._applyAlarmTimeoutId) - self._applyAlarmTimeoutId = None - self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_minutes_clicked(self, *args): - recurrenceChoices = [ - (1, "1 minute"), - (2, "2 minutes"), - (3, "3 minutes"), - (5, "5 minutes"), - (8, "8 minutes"), - (10, "10 minutes"), - (15, "15 minutes"), - (30, "30 minutes"), - (45, "45 minutes"), - (60, "1 hour"), - (3*60, "3 hours"), - (6*60, "6 hours"), - (12*60, "12 hours"), - ] - try: - actualSelection = self._alarmHandler.recurrence - - closestSelectionIndex = 0 - for i, possible in enumerate(recurrenceChoices): - if possible[0] <= actualSelection: - closestSelectionIndex = i - recurrenceIndex = hildonize.touch_selector( - self._window, - "Minutes", - (("%s" % m[1]) for m in recurrenceChoices), - closestSelectionIndex, - ) - recurrence = recurrenceChoices[recurrenceIndex][0] - - self._update_alarm_settings(recurrence) - except RuntimeError, e: - _moduleLogger.exception("%s" % str(e)) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_apply_timeout(self, *args): - try: - self._applyAlarmTimeoutId = None - - self._update_alarm_settings(self._alarmHandler.recurrence) - except Exception, e: - self._errorDisplay.push_exception() - return False - - def _on_missed_toggled(self, *args): - try: - self._notifyOnMissed = self._missedCheckbox.get_active() - self.save_everything() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_voicemail_toggled(self, *args): - try: - self._notifyOnVoicemail = self._voicemailCheckbox.get_active() - self.save_everything() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_sms_toggled(self, *args): - try: - self._notifyOnSms = self._smsCheckbox.get_active() - self.save_everything() - except Exception, e: - self._errorDisplay.push_exception() - - -class CallHistoryView(object): - - NUMBER_IDX = 0 - DATE_IDX = 1 - ACTION_IDX = 2 - FROM_IDX = 3 - FROM_ID_IDX = 4 - - HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"] - - def __init__(self, widgetTree, backend, errorDisplay): - self._errorDisplay = errorDisplay - self._backend = backend - - self._isPopulated = False - self._historymodel = gtk.ListStore( - gobject.TYPE_STRING, # number - gobject.TYPE_STRING, # date - gobject.TYPE_STRING, # action - gobject.TYPE_STRING, # from - gobject.TYPE_STRING, # from id - ) - self._historymodelfiltered = self._historymodel.filter_new() - self._historymodelfiltered.set_visible_func(self._is_history_visible) - self._historyview = widgetTree.get_widget("historyview") - self._historyviewselection = None - self._onRecentviewRowActivatedId = 0 - - textrenderer = gtk.CellRendererText() - textrenderer.set_property("yalign", 0) - self._dateColumn = gtk.TreeViewColumn("Date") - self._dateColumn.pack_start(textrenderer, expand=True) - self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX) - - textrenderer = gtk.CellRendererText() - textrenderer.set_property("yalign", 0) - self._actionColumn = gtk.TreeViewColumn("Action") - self._actionColumn.pack_start(textrenderer, expand=True) - self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX) - - textrenderer = gtk.CellRendererText() - textrenderer.set_property("yalign", 0) - textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END) - textrenderer.set_property("width-chars", len("1 (555) 555-1234")) - self._numberColumn = gtk.TreeViewColumn("Number") - self._numberColumn.pack_start(textrenderer, expand=True) - self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX) - - textrenderer = gtk.CellRendererText() - textrenderer.set_property("yalign", 0) - hildonize.set_cell_thumb_selectable(textrenderer) - self._nameColumn = gtk.TreeViewColumn("From") - self._nameColumn.pack_start(textrenderer, expand=True) - self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX) - self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - - self._window = gtk_toolbox.find_parent_window(self._historyview) - - self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector") - self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked) - self._selectedFilter = "All" - - self._updateSink = gtk_toolbox.threaded_stage( - gtk_toolbox.comap( - self._idly_populate_historyview, - gtk_toolbox.null_sink(), - ) - ) - - def enable(self): - assert self._backend.is_authed(), "Attempting to enable backend while not logged in" - self._historyFilterSelector.set_label(self._selectedFilter) - - self._historyview.set_model(self._historymodelfiltered) - self._historyview.set_fixed_height_mode(False) - - self._historyview.append_column(self._dateColumn) - self._historyview.append_column(self._actionColumn) - self._historyview.append_column(self._numberColumn) - self._historyview.append_column(self._nameColumn) - self._historyviewselection = self._historyview.get_selection() - self._historyviewselection.set_mode(gtk.SELECTION_SINGLE) - - self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated) - - def disable(self): - self._historyview.disconnect(self._onRecentviewRowActivatedId) - - self.clear() - - self._historyview.remove_column(self._dateColumn) - self._historyview.remove_column(self._actionColumn) - self._historyview.remove_column(self._nameColumn) - self._historyview.remove_column(self._numberColumn) - self._historyview.set_model(None) - - def add_contact(self, *args, **kwds): - """ - @note Actual dial function is patched in later - """ - raise NotImplementedError("Horrible unknown error has occurred") - - def update(self, force = False): - if not force and self._isPopulated: - return False - self._updateSink.send(()) - return True - - def clear(self): - self._isPopulated = False - self._historymodel.clear() - - @staticmethod - def name(): - return "Recent Calls" - - def load_settings(self, config, sectionName): - try: - self._selectedFilter = config.get(sectionName, "filter") - if self._selectedFilter not in self.HISTORY_ITEM_TYPES: - self._messageType = self.HISTORY_ITEM_TYPES[0] - except ConfigParser.NoOptionError: - pass - - def save_settings(self, config, sectionName): - """ - @note Thread Agnostic - """ - config.set(sectionName, "filter", self._selectedFilter) - - def set_orientation(self, orientation): - if orientation == gtk.ORIENTATION_VERTICAL: - pass - elif orientation == gtk.ORIENTATION_HORIZONTAL: - pass - else: - raise NotImplementedError(orientation) - - def _is_history_visible(self, model, iter): - try: - action = model.get_value(iter, self.ACTION_IDX) - if action is None: - return False # this seems weird but oh well - - if self._selectedFilter in [action, "All"]: - return True - else: - return False - except Exception, e: - self._errorDisplay.push_exception() - - def _idly_populate_historyview(self): - with gtk_toolbox.gtk_lock(): - banner = hildonize.show_busy_banner_start(self._window, "Loading Call History") - try: - self._historymodel.clear() - self._isPopulated = True - - try: - historyItems = self._backend.get_recent() - except Exception, e: - self._errorDisplay.push_exception_with_lock() - self._isPopulated = False - historyItems = [] - - historyItems = ( - gv_backend.decorate_recent(data) - for data in gv_backend.sort_messages(historyItems) - ) - - for contactId, personName, phoneNumber, date, action in historyItems: - if not personName: - personName = "Unknown" - date = abbrev_relative_date(date) - prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber - prettyNumber = make_pretty(prettyNumber) - item = (prettyNumber, date, action.capitalize(), personName, contactId) - with gtk_toolbox.gtk_lock(): - self._historymodel.append(item) - except Exception, e: - self._errorDisplay.push_exception_with_lock() - finally: - with gtk_toolbox.gtk_lock(): - hildonize.show_busy_banner_end(banner) - - return False - - def _on_history_filter_clicked(self, *args, **kwds): - try: - selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter) - - try: - newSelectedComboIndex = hildonize.touch_selector( - self._window, - "History", - self.HISTORY_ITEM_TYPES, - selectedComboIndex, - ) - except RuntimeError: - return - - option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex] - self._selectedFilter = option - self._historyFilterSelector.set_label(self._selectedFilter) - self._historymodelfiltered.refilter() - except Exception, e: - self._errorDisplay.push_exception() - - def _history_summary(self, expectedNumber): - for number, action, date, whoFrom, whoFromId in self._historymodel: - if expectedNumber is not None and expectedNumber == number: - yield "%s (%s) - %s %s" % (number, whoFrom, date, action) - - def _on_historyview_row_activated(self, treeview, path, view_column): - try: - childPath = self._historymodelfiltered.convert_path_to_child_path(path) - itr = self._historymodel.get_iter(childPath) - if not itr: - return - - prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX) - number = make_ugly(prettyNumber) - description = list(self._history_summary(prettyNumber)) - contactName = self._historymodel.get_value(itr, self.FROM_IDX) - contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX) - contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number) - - self.add_contact( - contactName, - contactPhoneNumbers, - messages = description, - defaultIndex = defaultIndex, - ) - self._historyviewselection.unselect_all() - except Exception, e: - self._errorDisplay.push_exception() - - -class MessagesView(object): - - NUMBER_IDX = 0 - DATE_IDX = 1 - HEADER_IDX = 2 - MESSAGE_IDX = 3 - MESSAGES_IDX = 4 - FROM_ID_IDX = 5 - MESSAGE_DATA_IDX = 6 - - NO_MESSAGES = "None" - VOICEMAIL_MESSAGES = "Voicemail" - TEXT_MESSAGES = "SMS" - ALL_TYPES = "All Messages" - MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES] - - UNREAD_STATUS = "Unread" - UNARCHIVED_STATUS = "Inbox" - ALL_STATUS = "Any" - MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS] - - def __init__(self, widgetTree, backend, errorDisplay): - self._errorDisplay = errorDisplay - self._backend = backend - - self._isPopulated = False - self._messagemodel = gtk.ListStore( - gobject.TYPE_STRING, # number - gobject.TYPE_STRING, # date - gobject.TYPE_STRING, # header - gobject.TYPE_STRING, # message - object, # messages - gobject.TYPE_STRING, # from id - object, # message data - ) - self._messagemodelfiltered = self._messagemodel.filter_new() - self._messagemodelfiltered.set_visible_func(self._is_message_visible) - self._messageview = widgetTree.get_widget("messages_view") - self._messageviewselection = None - self._onMessageviewRowActivatedId = 0 - - self._messageRenderer = gtk.CellRendererText() - self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD) - self._messageRenderer.set_property("wrap-width", 500) - self._messageColumn = gtk.TreeViewColumn("Messages") - self._messageColumn.pack_start(self._messageRenderer, expand=True) - self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX) - self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - - self._window = gtk_toolbox.find_parent_window(self._messageview) - - self._messageTypeButton = widgetTree.get_widget("messageTypeButton") - self._onMessageTypeClickedId = 0 - self._messageType = self.ALL_TYPES - self._messageStatusButton = widgetTree.get_widget("messageStatusButton") - self._onMessageStatusClickedId = 0 - self._messageStatus = self.ALL_STATUS - - self._updateSink = gtk_toolbox.threaded_stage( - gtk_toolbox.comap( - self._idly_populate_messageview, - gtk_toolbox.null_sink(), - ) - ) - - def enable(self): - assert self._backend.is_authed(), "Attempting to enable backend while not logged in" - self._messageview.set_model(self._messagemodelfiltered) - self._messageview.set_headers_visible(False) - self._messageview.set_fixed_height_mode(False) - - self._messageview.append_column(self._messageColumn) - self._messageviewselection = self._messageview.get_selection() - self._messageviewselection.set_mode(gtk.SELECTION_SINGLE) - - self._messageTypeButton.set_label(self._messageType) - self._messageStatusButton.set_label(self._messageStatus) - - self._onMessageviewRowActivatedId = self._messageview.connect( - "row-activated", self._on_messageview_row_activated - ) - self._onMessageTypeClickedId = self._messageTypeButton.connect( - "clicked", self._on_message_type_clicked - ) - self._onMessageStatusClickedId = self._messageStatusButton.connect( - "clicked", self._on_message_status_clicked - ) - - def disable(self): - self._messageview.disconnect(self._onMessageviewRowActivatedId) - self._messageTypeButton.disconnect(self._onMessageTypeClickedId) - self._messageStatusButton.disconnect(self._onMessageStatusClickedId) - - self.clear() - - self._messageview.remove_column(self._messageColumn) - self._messageview.set_model(None) - - def add_contact(self, *args, **kwds): - """ - @note Actual dial function is patched in later - """ - raise NotImplementedError("Horrible unknown error has occurred") - - def update(self, force = False): - if not force and self._isPopulated: - return False - self._updateSink.send(()) - return True - - def clear(self): - self._isPopulated = False - self._messagemodel.clear() - - @staticmethod - def name(): - return "Messages" - - def load_settings(self, config, sectionName): - try: - self._messageType = config.get(sectionName, "type") - if self._messageType not in self.MESSAGE_TYPES: - self._messageType = self.ALL_TYPES - self._messageStatus = config.get(sectionName, "status") - if self._messageStatus not in self.MESSAGE_STATUSES: - self._messageStatus = self.ALL_STATUS - except ConfigParser.NoOptionError: - pass - - def save_settings(self, config, sectionName): - """ - @note Thread Agnostic - """ - config.set(sectionName, "status", self._messageStatus) - config.set(sectionName, "type", self._messageType) - - def set_orientation(self, orientation): - if orientation == gtk.ORIENTATION_VERTICAL: - pass - elif orientation == gtk.ORIENTATION_HORIZONTAL: - pass - else: - raise NotImplementedError(orientation) - - def _is_message_visible(self, model, iter): - try: - message = model.get_value(iter, self.MESSAGE_DATA_IDX) - if message is None: - return False # this seems weird but oh well - return self._filter_messages(message, self._messageType, self._messageStatus) - except Exception, e: - self._errorDisplay.push_exception() - - @classmethod - def _filter_messages(cls, message, type, status): - if type == cls.ALL_TYPES: - isType = True - else: - messageType = message["type"] - isType = messageType == type - - if status == cls.ALL_STATUS: - isStatus = True - else: - isUnarchived = not message["isArchived"] - isUnread = not message["isRead"] - if status == cls.UNREAD_STATUS: - isStatus = isUnarchived and isUnread - elif status == cls.UNARCHIVED_STATUS: - isStatus = isUnarchived - else: - assert "Status %s is bad for %r" % (status, message) - - return isType and isStatus - - _MIN_MESSAGES_SHOWN = 4 - - def _idly_populate_messageview(self): - with gtk_toolbox.gtk_lock(): - banner = hildonize.show_busy_banner_start(self._window, "Loading Messages") - try: - self._messagemodel.clear() - self._isPopulated = True - - if self._messageType == self.NO_MESSAGES: - messageItems = [] - else: - try: - messageItems = self._backend.get_messages() - except Exception, e: - self._errorDisplay.push_exception_with_lock() - self._isPopulated = False - messageItems = [] - - messageItems = ( - (gv_backend.decorate_message(message), message) - for message in gv_backend.sort_messages(messageItems) - ) - - for (contactId, header, number, relativeDate, messages), messageData in messageItems: - prettyNumber = number[2:] if number.startswith("+1") else number - prettyNumber = make_pretty(prettyNumber) - - firstMessage = "%s - %s (%s)" % (header, prettyNumber, relativeDate) - expandedMessages = [firstMessage] - expandedMessages.extend(messages) - if (self._MIN_MESSAGES_SHOWN + 1) < len(messages): - firstMessage = "%s - %s (%s)" % (header, prettyNumber, relativeDate) - secondMessage = "%d Messages Hidden..." % (len(messages) - self._MIN_MESSAGES_SHOWN, ) - collapsedMessages = [firstMessage, secondMessage] - collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):]) - else: - collapsedMessages = expandedMessages - #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN) - - number = make_ugly(number) - - row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData - with gtk_toolbox.gtk_lock(): - self._messagemodel.append(row) - except Exception, e: - self._errorDisplay.push_exception_with_lock() - finally: - with gtk_toolbox.gtk_lock(): - hildonize.show_busy_banner_end(banner) - self._messagemodelfiltered.refilter() - - return False - - def _on_messageview_row_activated(self, treeview, path, view_column): - try: - childPath = self._messagemodelfiltered.convert_path_to_child_path(path) - itr = self._messagemodel.get_iter(childPath) - if not itr: - return - - number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX)) - description = self._messagemodel.get_value(itr, self.MESSAGES_IDX) - - contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX) - header = self._messagemodel.get_value(itr, self.HEADER_IDX) - contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number) - - self.add_contact( - header, - contactPhoneNumbers, - messages = description, - defaultIndex = defaultIndex, - ) - self._messageviewselection.unselect_all() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_message_type_clicked(self, *args, **kwds): - try: - selectedIndex = self.MESSAGE_TYPES.index(self._messageType) - - try: - newSelectedIndex = hildonize.touch_selector( - self._window, - "Message Type", - self.MESSAGE_TYPES, - selectedIndex, - ) - except RuntimeError: - return - - if selectedIndex != newSelectedIndex: - self._messageType = self.MESSAGE_TYPES[newSelectedIndex] - self._messageTypeButton.set_label(self._messageType) - self._messagemodelfiltered.refilter() - except Exception, e: - self._errorDisplay.push_exception() - - def _on_message_status_clicked(self, *args, **kwds): - try: - selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus) - - try: - newSelectedIndex = hildonize.touch_selector( - self._window, - "Message Status", - self.MESSAGE_STATUSES, - selectedIndex, - ) - except RuntimeError: - return - - if selectedIndex != newSelectedIndex: - self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex] - self._messageStatusButton.set_label(self._messageStatus) - self._messagemodelfiltered.refilter() - except Exception, e: - self._errorDisplay.push_exception() - - -class ContactsView(object): - - CONTACT_TYPE_IDX = 0 - CONTACT_NAME_IDX = 1 - CONTACT_ID_IDX = 2 - - def __init__(self, widgetTree, backend, errorDisplay): - self._errorDisplay = errorDisplay - self._backend = backend - - self._addressBook = None - self._selectedComboIndex = 0 - self._addressBookFactories = [null_backend.NullAddressBook()] - - self._booksList = [] - self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton") - - self._isPopulated = False - self._contactsmodel = gtk.ListStore( - gobject.TYPE_STRING, # Contact Type - gobject.TYPE_STRING, # Contact Name - gobject.TYPE_STRING, # Contact ID - ) - self._contactsviewselection = None - self._contactsview = widgetTree.get_widget("contactsview") - - self._contactColumn = gtk.TreeViewColumn("Contact") - displayContactSource = False - if displayContactSource: - textrenderer = gtk.CellRendererText() - self._contactColumn.pack_start(textrenderer, expand=False) - self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX) - textrenderer = gtk.CellRendererText() - hildonize.set_cell_thumb_selectable(textrenderer) - self._contactColumn.pack_start(textrenderer, expand=True) - self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX) - self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - self._contactColumn.set_sort_column_id(1) - self._contactColumn.set_visible(True) - - self._onContactsviewRowActivatedId = 0 - self._onAddressbookButtonChangedId = 0 - self._window = gtk_toolbox.find_parent_window(self._contactsview) - - self._updateSink = gtk_toolbox.threaded_stage( - gtk_toolbox.comap( - self._idly_populate_contactsview, - gtk_toolbox.null_sink(), - ) - ) - - def enable(self): - assert self._backend.is_authed(), "Attempting to enable backend while not logged in" - - self._contactsview.set_model(self._contactsmodel) - self._contactsview.set_fixed_height_mode(False) - self._contactsview.append_column(self._contactColumn) - self._contactsviewselection = self._contactsview.get_selection() - self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE) - - del self._booksList[:] - for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks(): - if factoryName and bookName: - entryName = "%s: %s" % (factoryName, bookName) - elif factoryName: - entryName = factoryName - elif bookName: - entryName = bookName - else: - entryName = "Bad name (%d)" % factoryId - row = (str(factoryId), bookId, entryName) - self._booksList.append(row) - - self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated) - self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed) - - if len(self._booksList) <= self._selectedComboIndex: - self._selectedComboIndex = 0 - self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2]) - - selectedFactoryId = self._booksList[self._selectedComboIndex][0] - selectedBookId = self._booksList[self._selectedComboIndex][1] - self.open_addressbook(selectedFactoryId, selectedBookId) - - def disable(self): - self._contactsview.disconnect(self._onContactsviewRowActivatedId) - self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId) - - self.clear() - - self._bookSelectionButton.set_label("") - self._contactsview.set_model(None) - self._contactsview.remove_column(self._contactColumn) - - def add_contact(self, *args, **kwds): - """ - @note Actual dial function is patched in later - """ - raise NotImplementedError("Horrible unknown error has occurred") - - def get_addressbooks(self): - """ - @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name)) - """ - for i, factory in enumerate(self._addressBookFactories): - for bookFactory, bookId, bookName in factory.get_addressbooks(): - yield (str(i), bookId), (factory.factory_name(), bookName) - - def open_addressbook(self, bookFactoryId, bookId): - bookFactoryIndex = int(bookFactoryId) - addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId) - self._addressBook = addressBook - - def update(self, force = False): - if not force and self._isPopulated: - return False - self._updateSink.send(()) - return True - - def clear(self): - self._isPopulated = False - self._contactsmodel.clear() - for factory in self._addressBookFactories: - factory.clear_caches() - self._addressBook.clear_caches() - - def append(self, book): - self._addressBookFactories.append(book) - - def extend(self, books): - self._addressBookFactories.extend(books) - - @staticmethod - def name(): - return "Contacts" - - def load_settings(self, config, sectionName): - try: - self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook") - except ConfigParser.NoOptionError: - self._selectedComboIndex = 0 - - def save_settings(self, config, sectionName): - config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex)) - - def set_orientation(self, orientation): - if orientation == gtk.ORIENTATION_VERTICAL: - pass - elif orientation == gtk.ORIENTATION_HORIZONTAL: - pass - else: - raise NotImplementedError(orientation) - - def _idly_populate_contactsview(self): - with gtk_toolbox.gtk_lock(): - banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts") - try: - addressBook = None - while addressBook is not self._addressBook: - addressBook = self._addressBook - with gtk_toolbox.gtk_lock(): - self._contactsview.set_model(None) - self.clear() - - try: - contacts = addressBook.get_contacts() - except Exception, e: - contacts = [] - self._isPopulated = False - self._errorDisplay.push_exception_with_lock() - for contactId, contactName in contacts: - contactType = addressBook.contact_source_short_name(contactId) - row = contactType, contactName, contactId - self._contactsmodel.append(row) - - with gtk_toolbox.gtk_lock(): - self._contactsview.set_model(self._contactsmodel) - - self._isPopulated = True - except Exception, e: - self._errorDisplay.push_exception_with_lock() - finally: - with gtk_toolbox.gtk_lock(): - hildonize.show_busy_banner_end(banner) - return False - - def _on_addressbook_button_changed(self, *args, **kwds): - try: - try: - newSelectedComboIndex = hildonize.touch_selector( - self._window, - "Addressbook", - (("%s" % m[2]) for m in self._booksList), - self._selectedComboIndex, - ) - except RuntimeError: - return - - selectedFactoryId = self._booksList[newSelectedComboIndex][0] - selectedBookId = self._booksList[newSelectedComboIndex][1] - - oldAddressbook = self._addressBook - self.open_addressbook(selectedFactoryId, selectedBookId) - forceUpdate = True if oldAddressbook is not self._addressBook else False - self.update(force=forceUpdate) - - self._selectedComboIndex = newSelectedComboIndex - self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2]) - except Exception, e: - self._errorDisplay.push_exception() - - def _on_contactsview_row_activated(self, treeview, path, view_column): - try: - itr = self._contactsmodel.get_iter(path) - if not itr: - return - - contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX) - contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX) - try: - contactDetails = self._addressBook.get_contact_details(contactId) - except Exception, e: - contactDetails = [] - self._errorDisplay.push_exception() - contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails] - - if len(contactPhoneNumbers) == 0: - return - - self.add_contact( - contactName, - contactPhoneNumbers, - messages = (contactName, ), - ) - self._contactsviewselection.unselect_all() - except Exception, e: - self._errorDisplay.push_exception() diff --git a/src/hildonize.py b/src/hildonize.py deleted file mode 100644 index 339eb2a..0000000 --- a/src/hildonize.py +++ /dev/null @@ -1,766 +0,0 @@ -#!/usr/bin/env python - -""" -Open Issues - @bug not all of a message is shown - @bug Buttons are too small -""" - - -import gobject -import gtk -import dbus - - -class _NullHildonModule(object): - pass - - -try: - import hildon as _hildon - hildon = _hildon # Dumb but gets around pyflakiness -except (ImportError, OSError): - hildon = _NullHildonModule - - -IS_HILDON_SUPPORTED = hildon is not _NullHildonModule - - -class _NullHildonProgram(object): - - def add_window(self, window): - pass - - -def _hildon_get_app_class(): - return hildon.Program - - -def _null_get_app_class(): - return _NullHildonProgram - - -try: - hildon.Program - get_app_class = _hildon_get_app_class -except AttributeError: - get_app_class = _null_get_app_class - - -def _hildon_set_application_name(name): - gtk.set_application_name(name) - - -def _null_set_application_name(name): - pass - - -try: - gtk.set_application_name - set_application_name = _hildon_set_application_name -except AttributeError: - set_application_name = _null_set_application_name - - -def _fremantle_hildonize_window(app, window): - oldWindow = window - newWindow = hildon.StackableWindow() - if oldWindow.get_child() is not None: - oldWindow.get_child().reparent(newWindow) - app.add_window(newWindow) - return newWindow - - -def _hildon_hildonize_window(app, window): - oldWindow = window - newWindow = hildon.Window() - if oldWindow.get_child() is not None: - oldWindow.get_child().reparent(newWindow) - app.add_window(newWindow) - return newWindow - - -def _null_hildonize_window(app, window): - return window - - -try: - hildon.StackableWindow - hildonize_window = _fremantle_hildonize_window -except AttributeError: - try: - hildon.Window - hildonize_window = _hildon_hildonize_window - except AttributeError: - hildonize_window = _null_hildonize_window - - -def _fremantle_hildonize_menu(window, gtkMenu): - appMenu = hildon.AppMenu() - window.set_app_menu(appMenu) - gtkMenu.get_parent().remove(gtkMenu) - return appMenu - - -def _hildon_hildonize_menu(window, gtkMenu): - hildonMenu = gtk.Menu() - for child in gtkMenu.get_children(): - child.reparent(hildonMenu) - window.set_menu(hildonMenu) - gtkMenu.destroy() - return hildonMenu - - -def _null_hildonize_menu(window, gtkMenu): - return gtkMenu - - -try: - hildon.AppMenu - GTK_MENU_USED = False - IS_FREMANTLE_SUPPORTED = True - hildonize_menu = _fremantle_hildonize_menu -except AttributeError: - GTK_MENU_USED = True - IS_FREMANTLE_SUPPORTED = False - if IS_HILDON_SUPPORTED: - hildonize_menu = _hildon_hildonize_menu - else: - hildonize_menu = _null_hildonize_menu - - -def _hildon_set_button_auto_selectable(button): - button.set_theme_size(hildon.HILDON_SIZE_AUTO_HEIGHT) - - -def _null_set_button_auto_selectable(button): - pass - - -try: - hildon.HILDON_SIZE_AUTO_HEIGHT - gtk.Button.set_theme_size - set_button_auto_selectable = _hildon_set_button_auto_selectable -except AttributeError: - set_button_auto_selectable = _null_set_button_auto_selectable - - -def _hildon_set_button_finger_selectable(button): - button.set_theme_size(hildon.HILDON_SIZE_FINGER_HEIGHT) - - -def _null_set_button_finger_selectable(button): - pass - - -try: - hildon.HILDON_SIZE_FINGER_HEIGHT - gtk.Button.set_theme_size - set_button_finger_selectable = _hildon_set_button_finger_selectable -except AttributeError: - set_button_finger_selectable = _null_set_button_finger_selectable - - -def _hildon_set_button_thumb_selectable(button): - button.set_theme_size(hildon.HILDON_SIZE_THUMB_HEIGHT) - - -def _null_set_button_thumb_selectable(button): - pass - - -try: - hildon.HILDON_SIZE_THUMB_HEIGHT - gtk.Button.set_theme_size - set_button_thumb_selectable = _hildon_set_button_thumb_selectable -except AttributeError: - set_button_thumb_selectable = _null_set_button_thumb_selectable - - -def _hildon_set_cell_thumb_selectable(renderer): - renderer.set_property("scale", 1.5) - - -def _null_set_cell_thumb_selectable(renderer): - pass - - -if IS_HILDON_SUPPORTED: - set_cell_thumb_selectable = _hildon_set_cell_thumb_selectable -else: - set_cell_thumb_selectable = _null_set_cell_thumb_selectable - - -def _hildon_set_pix_cell_thumb_selectable(renderer): - renderer.set_property("stock-size", 48) - - -def _null_set_pix_cell_thumb_selectable(renderer): - pass - - -if IS_HILDON_SUPPORTED: - set_pix_cell_thumb_selectable = _hildon_set_pix_cell_thumb_selectable -else: - set_pix_cell_thumb_selectable = _null_set_pix_cell_thumb_selectable - - -def _fremantle_show_information_banner(parent, message): - hildon.hildon_banner_show_information(parent, "", message) - - -def _hildon_show_information_banner(parent, message): - hildon.hildon_banner_show_information(parent, None, message) - - -def _null_show_information_banner(parent, message): - pass - - -if IS_FREMANTLE_SUPPORTED: - show_information_banner = _fremantle_show_information_banner -else: - try: - hildon.hildon_banner_show_information - show_information_banner = _hildon_show_information_banner - except AttributeError: - show_information_banner = _null_show_information_banner - - -def _fremantle_show_busy_banner_start(parent, message): - hildon.hildon_gtk_window_set_progress_indicator(parent, True) - return parent - - -def _fremantle_show_busy_banner_end(banner): - hildon.hildon_gtk_window_set_progress_indicator(banner, False) - - -def _hildon_show_busy_banner_start(parent, message): - return hildon.hildon_banner_show_animation(parent, None, message) - - -def _hildon_show_busy_banner_end(banner): - banner.destroy() - - -def _null_show_busy_banner_start(parent, message): - return None - - -def _null_show_busy_banner_end(banner): - assert banner is None - - -try: - hildon.hildon_gtk_window_set_progress_indicator - show_busy_banner_start = _fremantle_show_busy_banner_start - show_busy_banner_end = _fremantle_show_busy_banner_end -except AttributeError: - try: - hildon.hildon_banner_show_animation - show_busy_banner_start = _hildon_show_busy_banner_start - show_busy_banner_end = _hildon_show_busy_banner_end - except AttributeError: - show_busy_banner_start = _null_show_busy_banner_start - show_busy_banner_end = _null_show_busy_banner_end - - -def _hildon_hildonize_text_entry(textEntry): - textEntry.set_property('hildon-input-mode', 7) - - -def _null_hildonize_text_entry(textEntry): - pass - - -if IS_HILDON_SUPPORTED: - hildonize_text_entry = _hildon_hildonize_text_entry -else: - hildonize_text_entry = _null_hildonize_text_entry - - -def _hildon_window_to_portrait(window): - # gtk documentation is unclear whether this does a "=" or a "|=" - flags = hildon.PORTRAIT_MODE_SUPPORT | hildon.PORTRAIT_MODE_REQUEST - hildon.hildon_gtk_window_set_portrait_flags(window, flags) - - -def _hildon_window_to_landscape(window): - # gtk documentation is unclear whether this does a "=" or a "&= ~" - flags = hildon.PORTRAIT_MODE_SUPPORT - hildon.hildon_gtk_window_set_portrait_flags(window, flags) - - -def _null_window_to_portrait(window): - pass - - -def _null_window_to_landscape(window): - pass - - -try: - hildon.PORTRAIT_MODE_SUPPORT - hildon.PORTRAIT_MODE_REQUEST - hildon.hildon_gtk_window_set_portrait_flags - - window_to_portrait = _hildon_window_to_portrait - window_to_landscape = _hildon_window_to_landscape -except AttributeError: - window_to_portrait = _null_window_to_portrait - window_to_landscape = _null_window_to_landscape - - -def get_device_orientation(): - bus = dbus.SystemBus() - try: - rawMceRequest = bus.get_object("com.nokia.mce", "/com/nokia/mce/request") - mceRequest = dbus.Interface(rawMceRequest, dbus_interface="com.nokia.mce.request") - orientation, standState, faceState, xAxis, yAxis, zAxis = mceRequest.get_device_orientation() - except dbus.exception.DBusException: - # catching for documentation purposes that when a system doesn't - # support this, this is what to expect - raise - - if orientation == "": - return gtk.ORIENTATION_HORIZONTAL - elif orientation == "": - return gtk.ORIENTATION_VERTICAL - else: - raise RuntimeError("Unknown orientation: %s" % orientation) - - -def _hildon_hildonize_password_entry(textEntry): - textEntry.set_property('hildon-input-mode', 7 | (1 << 29)) - - -def _null_hildonize_password_entry(textEntry): - pass - - -if IS_HILDON_SUPPORTED: - hildonize_password_entry = _hildon_hildonize_password_entry -else: - hildonize_password_entry = _null_hildonize_password_entry - - -def _hildon_hildonize_combo_entry(comboEntry): - comboEntry.set_property('hildon-input-mode', 1 << 4) - - -def _null_hildonize_combo_entry(textEntry): - pass - - -if IS_HILDON_SUPPORTED: - hildonize_combo_entry = _hildon_hildonize_combo_entry -else: - hildonize_combo_entry = _null_hildonize_combo_entry - - -def _null_create_seekbar(): - adjustment = gtk.Adjustment(0, 0, 101, 1, 5, 1) - seek = gtk.HScale(adjustment) - seek.set_draw_value(False) - return seek - - -def _fremantle_create_seekbar(): - seek = hildon.Seekbar() - seek.set_range(0.0, 100) - seek.set_draw_value(False) - seek.set_update_policy(gtk.UPDATE_DISCONTINUOUS) - return seek - - -try: - hildon.Seekbar - create_seekbar = _fremantle_create_seekbar -except AttributeError: - create_seekbar = _null_create_seekbar - - -def _fremantle_hildonize_scrollwindow(scrolledWindow): - pannableWindow = hildon.PannableArea() - - child = scrolledWindow.get_child() - scrolledWindow.remove(child) - pannableWindow.add(child) - - parent = scrolledWindow.get_parent() - if parent is not None: - parent.remove(scrolledWindow) - parent.add(pannableWindow) - - return pannableWindow - - -def _hildon_hildonize_scrollwindow(scrolledWindow): - hildon.hildon_helper_set_thumb_scrollbar(scrolledWindow, True) - return scrolledWindow - - -def _null_hildonize_scrollwindow(scrolledWindow): - return scrolledWindow - - -try: - hildon.PannableArea - hildonize_scrollwindow = _fremantle_hildonize_scrollwindow - hildonize_scrollwindow_with_viewport = _hildon_hildonize_scrollwindow -except AttributeError: - try: - hildon.hildon_helper_set_thumb_scrollbar - hildonize_scrollwindow = _hildon_hildonize_scrollwindow - hildonize_scrollwindow_with_viewport = _hildon_hildonize_scrollwindow - except AttributeError: - hildonize_scrollwindow = _null_hildonize_scrollwindow - hildonize_scrollwindow_with_viewport = _null_hildonize_scrollwindow - - -def _hildon_request_number(parent, title, range, default): - spinner = hildon.NumberEditor(*range) - spinner.set_value(default) - - dialog = gtk.Dialog( - title, - parent, - gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), - ) - dialog.set_default_response(gtk.RESPONSE_CANCEL) - dialog.get_child().add(spinner) - - try: - dialog.show_all() - response = dialog.run() - - if response == gtk.RESPONSE_OK: - return spinner.get_value() - elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: - raise RuntimeError("User cancelled request") - else: - raise RuntimeError("Unrecognized response %r", response) - finally: - dialog.hide() - dialog.destroy() - - -def _null_request_number(parent, title, range, default): - adjustment = gtk.Adjustment(default, range[0], range[1], 1, 5, 0) - spinner = gtk.SpinButton(adjustment, 0, 0) - spinner.set_wrap(False) - - dialog = gtk.Dialog( - title, - parent, - gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), - ) - dialog.set_default_response(gtk.RESPONSE_CANCEL) - dialog.get_child().add(spinner) - - try: - dialog.show_all() - response = dialog.run() - - if response == gtk.RESPONSE_OK: - return spinner.get_value_as_int() - elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: - raise RuntimeError("User cancelled request") - else: - raise RuntimeError("Unrecognized response %r", response) - finally: - dialog.hide() - dialog.destroy() - - -try: - hildon.NumberEditor # TODO deprecated in fremantle - request_number = _hildon_request_number -except AttributeError: - request_number = _null_request_number - - -def _hildon_touch_selector(parent, title, items, defaultIndex): - model = gtk.ListStore(gobject.TYPE_STRING) - for item in items: - model.append((item, )) - - selector = hildon.TouchSelector() - selector.append_text_column(model, True) - selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE) - selector.set_active(0, defaultIndex) - - dialog = hildon.PickerDialog(parent) - dialog.set_selector(selector) - - try: - dialog.show_all() - response = dialog.run() - - if response == gtk.RESPONSE_OK: - return selector.get_active(0) - elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: - raise RuntimeError("User cancelled request") - else: - raise RuntimeError("Unrecognized response %r", response) - finally: - dialog.hide() - dialog.destroy() - - -def _on_null_touch_selector_activated(treeView, path, column, dialog, pathData): - dialog.response(gtk.RESPONSE_OK) - pathData[0] = path - - -def _null_touch_selector(parent, title, items, defaultIndex = -1): - parentSize = parent.get_size() - - model = gtk.ListStore(gobject.TYPE_STRING) - for item in items: - model.append((item, )) - - cell = gtk.CellRendererText() - set_cell_thumb_selectable(cell) - column = gtk.TreeViewColumn(title) - column.pack_start(cell, expand=True) - column.add_attribute(cell, "text", 0) - - treeView = gtk.TreeView() - treeView.set_model(model) - treeView.append_column(column) - selection = treeView.get_selection() - selection.set_mode(gtk.SELECTION_SINGLE) - if 0 < defaultIndex: - selection.select_path((defaultIndex, )) - - scrolledWin = gtk.ScrolledWindow() - scrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scrolledWin.add(treeView) - - dialog = gtk.Dialog( - title, - parent, - gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), - ) - dialog.set_default_response(gtk.RESPONSE_CANCEL) - dialog.get_child().add(scrolledWin) - dialog.resize(parentSize[0], max(parentSize[1]-100, 100)) - - scrolledWin = hildonize_scrollwindow(scrolledWin) - pathData = [None] - treeView.connect("row-activated", _on_null_touch_selector_activated, dialog, pathData) - - try: - dialog.show_all() - response = dialog.run() - - if response == gtk.RESPONSE_OK: - if pathData[0] is None: - raise RuntimeError("No selection made") - return pathData[0][0] - elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: - raise RuntimeError("User cancelled request") - else: - raise RuntimeError("Unrecognized response %r", response) - finally: - dialog.hide() - dialog.destroy() - - -try: - hildon.PickerDialog - hildon.TouchSelector - touch_selector = _hildon_touch_selector -except AttributeError: - touch_selector = _null_touch_selector - - -def _hildon_touch_selector_entry(parent, title, items, defaultItem): - # Got a segfault when using append_text_column with TouchSelectorEntry, so using this way - try: - selector = hildon.TouchSelectorEntry(text=True) - except TypeError: - selector = hildon.hildon_touch_selector_entry_new_text() - defaultIndex = -1 - for i, item in enumerate(items): - selector.append_text(item) - if item == defaultItem: - defaultIndex = i - - dialog = hildon.PickerDialog(parent) - dialog.set_selector(selector) - - if 0 < defaultIndex: - selector.set_active(0, defaultIndex) - else: - selector.get_entry().set_text(defaultItem) - - try: - dialog.show_all() - response = dialog.run() - finally: - dialog.hide() - - if response == gtk.RESPONSE_OK: - return selector.get_entry().get_text() - elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: - raise RuntimeError("User cancelled request") - else: - raise RuntimeError("Unrecognized response %r", response) - - -def _on_null_touch_selector_entry_entry_changed(entry, result, selection, defaultIndex): - custom = entry.get_text().strip() - if custom: - result[0] = custom - selection.unselect_all() - else: - result[0] = None - selection.select_path((defaultIndex, )) - - -def _on_null_touch_selector_entry_entry_activated(customEntry, dialog, result): - dialog.response(gtk.RESPONSE_OK) - result[0] = customEntry.get_text() - - -def _on_null_touch_selector_entry_tree_activated(treeView, path, column, dialog, result): - dialog.response(gtk.RESPONSE_OK) - model = treeView.get_model() - itr = model.get_iter(path) - if itr is not None: - result[0] = model.get_value(itr, 0) - - -def _null_touch_selector_entry(parent, title, items, defaultItem): - parentSize = parent.get_size() - - model = gtk.ListStore(gobject.TYPE_STRING) - defaultIndex = -1 - for i, item in enumerate(items): - model.append((item, )) - if item == defaultItem: - defaultIndex = i - - cell = gtk.CellRendererText() - set_cell_thumb_selectable(cell) - column = gtk.TreeViewColumn(title) - column.pack_start(cell, expand=True) - column.add_attribute(cell, "text", 0) - - treeView = gtk.TreeView() - treeView.set_model(model) - treeView.append_column(column) - selection = treeView.get_selection() - selection.set_mode(gtk.SELECTION_SINGLE) - - scrolledWin = gtk.ScrolledWindow() - scrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scrolledWin.add(treeView) - - customEntry = gtk.Entry() - - layout = gtk.VBox() - layout.pack_start(customEntry, expand=False) - layout.pack_start(scrolledWin) - - dialog = gtk.Dialog( - title, - parent, - gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), - ) - dialog.set_default_response(gtk.RESPONSE_CANCEL) - dialog.get_child().add(layout) - dialog.resize(parentSize[0], max(parentSize[1]-100, 100)) - - scrolledWin = hildonize_scrollwindow(scrolledWin) - - result = [None] - if 0 < defaultIndex: - selection.select_path((defaultIndex, )) - result[0] = defaultItem - else: - customEntry.set_text(defaultItem) - - customEntry.connect("activate", _on_null_touch_selector_entry_entry_activated, dialog, result) - customEntry.connect("changed", _on_null_touch_selector_entry_entry_changed, result, selection, defaultIndex) - treeView.connect("row-activated", _on_null_touch_selector_entry_tree_activated, dialog, result) - - try: - dialog.show_all() - response = dialog.run() - - if response == gtk.RESPONSE_OK: - _, itr = selection.get_selected() - if itr is not None: - return model.get_value(itr, 0) - else: - enteredText = customEntry.get_text().strip() - if enteredText: - return enteredText - elif result[0] is not None: - return result[0] - else: - raise RuntimeError("No selection made") - elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: - raise RuntimeError("User cancelled request") - else: - raise RuntimeError("Unrecognized response %r", response) - finally: - dialog.hide() - dialog.destroy() - - -try: - hildon.PickerDialog - hildon.TouchSelectorEntry - touch_selector_entry = _hildon_touch_selector_entry -except AttributeError: - touch_selector_entry = _null_touch_selector_entry - - -if __name__ == "__main__": - app = get_app_class()() - - label = gtk.Label("Hello World from a Label!") - - win = gtk.Window() - win.add(label) - win = hildonize_window(app, win) - if False and IS_FREMANTLE_SUPPORTED: - appMenu = hildon.AppMenu() - for i in xrange(5): - b = gtk.Button(str(i)) - appMenu.append(b) - win.set_app_menu(appMenu) - win.show_all() - appMenu.show_all() - gtk.main() - elif False: - print touch_selector(win, "Test", ["A", "B", "C", "D"], 2) - elif False: - print touch_selector_entry(win, "Test", ["A", "B", "C", "D"], "C") - print touch_selector_entry(win, "Test", ["A", "B", "C", "D"], "Blah") - elif False: - import pprint - name, value = "", "" - goodLocals = [ - (name, value) for (name, value) in locals().iteritems() - if not name.startswith("_") - ] - pprint.pprint(goodLocals) - elif False: - import time - show_information_banner(win, "Hello World") - time.sleep(5) - elif False: - import time - banner = show_busy_banner_start(win, "Hello World") - time.sleep(5) - show_busy_banner_end(banner) diff --git a/src/maeqt.py b/src/maeqt.py new file mode 100644 index 0000000..d5eb18b --- /dev/null +++ b/src/maeqt.py @@ -0,0 +1,122 @@ +from PyQt4 import QtCore +from PyQt4 import QtGui + + +def _null_set_stackable(window, isStackable): + pass + + +def _maemo_set_stackable(window, isStackable): + window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable) + + +try: + QtCore.Qt.WA_Maemo5StackedWindow + set_stackable = _maemo_set_stackable +except AttributeError: + set_stackable = _null_set_stackable + + +def _null_set_autorient(window, isStackable): + pass + + +def _maemo_set_autorient(window, isStackable): + window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable) + + +try: + QtCore.Qt.WA_Maemo5AutoOrientation + set_autorient = _maemo_set_autorient +except AttributeError: + set_autorient = _null_set_autorient + + +def _null_set_landscape(window, isStackable): + pass + + +def _maemo_set_landscape(window, isStackable): + window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable) + + +try: + QtCore.Qt.WA_Maemo5LandscapeOrientation + set_landscape = _maemo_set_landscape +except AttributeError: + set_landscape = _null_set_landscape + + +def _null_set_portrait(window, isStackable): + pass + + +def _maemo_set_portrait(window, isStackable): + window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable) + + +try: + QtCore.Qt.WA_Maemo5PortraitOrientation + set_portrait = _maemo_set_portrait +except AttributeError: + set_portrait = _null_set_portrait + + +def _null_show_progress_indicator(window, isStackable): + pass + + +def _maemo_show_progress_indicator(window, isStackable): + window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable) + + +try: + QtCore.Qt.WA_Maemo5ShowProgressIndicator + show_progress_indicator = _maemo_show_progress_indicator +except AttributeError: + show_progress_indicator = _null_show_progress_indicator + + +def _null_mark_numbers_preferred(widget): + pass + + +def _newqt_mark_numbers_preferred(widget): + widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers) + + +try: + QtCore.Qt.ImhPreferNumbers + mark_numbers_preferred = _newqt_mark_numbers_preferred +except AttributeError: + mark_numbers_preferred = _null_mark_numbers_preferred + + +def screen_orientation(): + geom = QtGui.QApplication.desktop().screenGeometry() + if geom.width() <= geom.height(): + return QtCore.Qt.Vertical + else: + return QtCore.Qt.Horizontal + + +def _null_get_theme_icon(iconNames, fallback = None): + icon = fallback if fallback is not None else QtGui.QIcon() + return icon + + +def _newqt_get_theme_icon(iconNames, fallback = None): + for iconName in iconNames: + if QtGui.QIcon.hasThemeIcon(iconName): + icon = QtGui.QIcon.fromTheme(iconName) + break + else: + icon = fallback if fallback is not None else QtGui.QIcon() + return icon + + +try: + QtGui.QIcon.fromTheme + get_theme_icon = _newqt_get_theme_icon +except AttributeError: + get_theme_icon = _null_get_theme_icon diff --git a/src/null_views.py b/src/null_views.py deleted file mode 100644 index 2299b28..0000000 --- a/src/null_views.py +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/python2.5 - -""" -DialCentral - Front end for Google's Grand Central service. -Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -""" - -import gobject -import gtk - - -class Dialpad(object): - - def __init__(self, widgetTree): - self._buttons = [ - widgetTree.get_widget(buttonName) - for buttonName in ("dialpadCall", "dialpadSMS") - ] - self._numberdisplay = widgetTree.get_widget("numberdisplay") - - def enable(self): - for button in self._buttons: - button.set_sensitive(False) - - def disable(self): - for button in self._buttons: - button.set_sensitive(True) - - @staticmethod - def name(): - return "Dialpad" - - def load_settings(self, config, sectionName): - pass - - def save_settings(self, config, sectionName): - """ - @note Thread Agnostic - """ - pass - - def set_orientation(self, orientation): - if orientation == gtk.ORIENTATION_VERTICAL: - pass - elif orientation == gtk.ORIENTATION_HORIZONTAL: - pass - else: - raise NotImplementedError(orientation) - - -class AccountInfo(object): - - def __init__(self, widgetTree): - self._callbackList = gtk.ListStore(gobject.TYPE_STRING) - self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display") - self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton") - self._clearCookiesButton = widgetTree.get_widget("clearcookies") - - self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox") - self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton") - self._missedCheckbox = widgetTree.get_widget("missedCheckbox") - self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox") - self._smsCheckbox = widgetTree.get_widget("smsCheckbox") - - def enable(self): - self._callbackSelectButton.set_sensitive(False) - self._clearCookiesButton.set_sensitive(False) - - self._notifyCheckbox.set_sensitive(False) - self._minutesEntryButton.set_sensitive(False) - self._missedCheckbox.set_sensitive(False) - self._voicemailCheckbox.set_sensitive(False) - self._smsCheckbox.set_sensitive(False) - - self._accountViewNumberDisplay.set_label("") - - def disable(self): - self._callbackSelectButton.set_sensitive(True) - self._clearCookiesButton.set_sensitive(True) - - self._notifyCheckbox.set_sensitive(True) - self._minutesEntryButton.set_sensitive(True) - self._missedCheckbox.set_sensitive(True) - self._voicemailCheckbox.set_sensitive(True) - self._smsCheckbox.set_sensitive(True) - - @staticmethod - def update(force = False): - return False - - @staticmethod - def clear(): - pass - - @staticmethod - def name(): - return "Account Info" - - def load_settings(self, config, sectionName): - pass - - def save_settings(self, config, sectionName): - """ - @note Thread Agnostic - """ - pass - - def set_orientation(self, orientation): - if orientation == gtk.ORIENTATION_VERTICAL: - pass - elif orientation == gtk.ORIENTATION_HORIZONTAL: - pass - else: - raise NotImplementedError(orientation) - - -class CallHistoryView(object): - - def __init__(self, widgetTree): - self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector") - - def enable(self): - self._historyFilterSelector.set_sensitive(False) - - def disable(self): - self._historyFilterSelector.set_sensitive(True) - - def update(self, force = False): - return False - - @staticmethod - def clear(): - pass - - @staticmethod - def name(): - return "Recent Calls" - - def load_settings(self, config, sectionName): - pass - - def save_settings(self, config, sectionName): - """ - @note Thread Agnostic - """ - pass - - def set_orientation(self, orientation): - if orientation == gtk.ORIENTATION_VERTICAL: - pass - elif orientation == gtk.ORIENTATION_HORIZONTAL: - pass - else: - raise NotImplementedError(orientation) - - -class MessagesView(object): - - def __init__(self, widgetTree): - self._messageTypeButton = widgetTree.get_widget("messageTypeButton") - self._messageStatusButton = widgetTree.get_widget("messageStatusButton") - - def enable(self): - self._messageTypeButton.set_sensitive(False) - self._messageStatusButton.set_sensitive(False) - - def disable(self): - self._messageTypeButton.set_sensitive(True) - self._messageStatusButton.set_sensitive(True) - - def update(self, force = False): - return False - - @staticmethod - def clear(): - pass - - @staticmethod - def name(): - return "Messages" - - def load_settings(self, config, sectionName): - pass - - def save_settings(self, config, sectionName): - """ - @note Thread Agnostic - """ - pass - - def set_orientation(self, orientation): - if orientation == gtk.ORIENTATION_VERTICAL: - pass - elif orientation == gtk.ORIENTATION_HORIZONTAL: - pass - else: - raise NotImplementedError(orientation) - - -class ContactsView(object): - - def __init__(self, widgetTree): - self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton") - - def enable(self): - self._bookSelectionButton.set_sensitive(False) - - def disable(self): - self._bookSelectionButton.set_sensitive(True) - - def update(self, force = False): - return False - - @staticmethod - def clear(): - pass - - @staticmethod - def name(): - return "Contacts" - - def load_settings(self, config, sectionName): - pass - - def save_settings(self, config, sectionName): - """ - @note Thread Agnostic - """ - pass - - def set_orientation(self, orientation): - if orientation == gtk.ORIENTATION_VERTICAL: - pass - elif orientation == gtk.ORIENTATION_HORIZONTAL: - pass - else: - raise NotImplementedError(orientation) -- 1.7.9.5