+++ /dev/null
-"""
-@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
import itertools
import logging
-import gvoice
+from gvoice import gvoice
-_moduleLogger = logging.getLogger("gv_backend")
+_moduleLogger = logging.getLogger(__name__)
class GVDialer(object):
+++ /dev/null
-#!/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"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
- 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"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
- self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
- self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
- self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
-
- self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
- self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
- self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
- self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
- self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
- self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
- self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
- self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
- self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
- self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
- self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
- self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
- self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", 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()
--- /dev/null
+"""
+@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
--- /dev/null
+#!/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"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
+ 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"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
+ self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
+ self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
+ self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
+
+ self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
+ self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
+ self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
+ self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
+ self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
+ self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
+ self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
+ self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
+ self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
+ self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
+ self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+ self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+ self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", 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()
import logging
-_moduleLogger = logging.getLogger("merge_backend")
+_moduleLogger = logging.getLogger(__name__)
class MergedAddressBook(object):
+++ /dev/null
-#!/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(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <eopage@byu.net>"])
- 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()
+++ /dev/null
-<?xml version="1.0"?>
-<glade-interface>
- <!-- interface-requires gtk+ 2.16 -->
- <!-- interface-naming-policy toplevel-contextual -->
- <widget class="GtkWindow" id="mainWindow">
- <property name="title" translatable="yes">Dialer</property>
- <property name="default_width">800</property>
- <property name="default_height">480</property>
- <child>
- <widget class="GtkVBox" id="mainLayout">
- <property name="visible">True</property>
- <child>
- <widget class="GtkMenuBar" id="dialpad_menubar">
- <property name="visible">True</property>
- <child>
- <widget class="GtkMenuItem" id="login_menu_item">
- <property name="visible">True</property>
- <property name="label">_Login</property>
- <property name="use_underline">True</property>
- <signal name="activate" handler="on_clearcookies_clicked"/>
- </widget>
- </child>
- <child>
- <widget class="GtkMenuItem" id="paste_menu_item">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Paste</property>
- <property name="use_underline">True</property>
- <signal name="activate" handler="on_paste"/>
- </widget>
- </child>
- <child>
- <widget class="GtkMenuItem" id="refreshMenuItem">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Refresh</property>
- <property name="use_underline">True</property>
- <signal name="activate" handler="on_refresh"/>
- </widget>
- </child>
- <child>
- <widget class="GtkMenuItem" id="about">
- <property name="visible">True</property>
- <property name="label" translatable="yes">_About</property>
- <property name="use_underline">True</property>
- <signal name="activate" handler="on_about_activate"/>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkEventBox" id="errorEventBox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkHBox" id="errorBox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="errorImage">
- <property name="visible">True</property>
- <property name="stock">gtk-dialog-error</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="errorDescription">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Unknown Error</property>
- <property name="use_markup">True</property>
- <property name="ellipsize">end</property>
- <property name="single_line_mode">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkImage" id="errorClose">
- <property name="visible">True</property>
- <property name="stock">gtk-close</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkNotebook" id="notebook">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="tab_pos">left</property>
- <property name="show_border">False</property>
- <property name="homogeneous">True</property>
- <child>
- <widget class="GtkVBox" id="keypad_vbox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkHBox" id="hbox3">
- <property name="visible">True</property>
- <child>
- <widget class="GtkButton" id="plus">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <property name="focus_on_click">False</property>
- <accelerator key="0" signal="clicked"/>
- <child>
- <widget class="GtkHBox" id="hbox5">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="image3">
- <property name="visible">True</property>
- <property name="tooltip" translatable="yes">+</property>
- <property name="xalign">1</property>
- <property name="stock">gtk-add</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="numberdisplay">
- <property name="height_request">50</property>
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="35000" weight="bold"></span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="back">
- <property name="label" translatable="yes">gtk-go-back</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <property name="use_stock">True</property>
- <property name="focus_on_click">False</property>
- <accelerator key="BackSpace" signal="clicked"/>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkTable" id="keypadview">
- <property name="visible">True</property>
- <property name="n_rows">4</property>
- <property name="n_columns">3</property>
- <property name="homogeneous">True</property>
- <child>
- <widget class="GtkButton" id="digit1">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="1" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label12">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="33000" weight="bold">1</span>
-<span size="9000"> </span></property>
- <property name="use_markup">True</property>
- </widget>
- </child>
- </widget>
- </child>
- <child>
- <widget class="GtkButton" id="digit2">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="c" signal="clicked"/>
- <accelerator key="b" signal="clicked"/>
- <accelerator key="a" signal="clicked"/>
- <accelerator key="2" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label10">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">2</span>
-<span size="12000">ABC</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit3">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="f" signal="clicked"/>
- <accelerator key="e" signal="clicked"/>
- <accelerator key="d" signal="clicked"/>
- <accelerator key="3" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label11">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold" stretch="ultraexpanded">3</span>
-<span size="12000">DEF</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">2</property>
- <property name="right_attach">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit4">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="i" signal="clicked"/>
- <accelerator key="h" signal="clicked"/>
- <accelerator key="g" signal="clicked"/>
- <accelerator key="4" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label13">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">4</span>
-<span size="12000">GHI</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit5">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="l" signal="clicked"/>
- <accelerator key="k" signal="clicked"/>
- <accelerator key="j" signal="clicked"/>
- <accelerator key="5" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label14">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">5</span>
-<span size="12000">JKL</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit6">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="o" signal="clicked"/>
- <accelerator key="n" signal="clicked"/>
- <accelerator key="m" signal="clicked"/>
- <accelerator key="6" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label15">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">6</span>
-<span size="12000">MNO</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">2</property>
- <property name="right_attach">3</property>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit7">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="s" signal="clicked"/>
- <accelerator key="r" signal="clicked"/>
- <accelerator key="q" signal="clicked"/>
- <accelerator key="p" signal="clicked"/>
- <accelerator key="7" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label16">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">7</span>
-<span size="12000">PQRS</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit8">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="v" signal="clicked"/>
- <accelerator key="u" signal="clicked"/>
- <accelerator key="t" signal="clicked"/>
- <accelerator key="8" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label17">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">8</span>
-<span size="12000">TUV</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit9">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="z" signal="clicked"/>
- <accelerator key="y" signal="clicked"/>
- <accelerator key="x" signal="clicked"/>
- <accelerator key="w" signal="clicked"/>
- <accelerator key="9" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label18">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="30000" weight="bold">9</span>
-<span size="12000">WXYZ</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">2</property>
- <property name="right_attach">3</property>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="digit0">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <property name="focus_on_click">False</property>
- <signal name="clicked" handler="on_digit_clicked"/>
- <accelerator key="0" signal="clicked"/>
- <child>
- <widget class="GtkLabel" id="label19">
- <property name="visible">True</property>
- <property name="label" translatable="yes"><span size="33000" weight="bold">0</span></property>
- <property name="use_markup">True</property>
- <property name="justify">center</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="dialpadCall">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="receives_default">False</property>
- <accelerator key="Return" signal="clicked"/>
- <child>
- <widget class="GtkHBox" id="hbox1">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="image1">
- <property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="stock">gtk-yes</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label8">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="xpad">5</property>
- <property name="label" translatable="yes"><span size="17000" weight="bold">Call</span></property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="left_attach">2</property>
- <property name="right_attach">3</property>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="dialpadSMS">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <accelerator key="Return" signal="clicked"/>
- <child>
- <widget class="GtkHBox" id="hbox2">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="image2">
- <property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="stock">gtk-select-font</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label2">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="xpad">5</property>
- <property name="label" translatable="yes"><span size="17000" weight="bold">SMS</span></property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="keypad">
- <property name="height_request">30</property>
- <property name="visible">True</property>
- <property name="label" translatable="yes">Keypad</property>
- </widget>
- <packing>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- <property name="type">tab</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="vbox2">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkButton" id="historyFilterSelector">
- <property name="label" translatable="yes">All</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkScrolledWindow" id="history_scrolledwindow">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="hscrollbar_policy">never</property>
- <child>
- <widget class="GtkTreeView" id="historyview">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="rules_hint">True</property>
- <property name="enable_grid_lines">horizontal</property>
- <property name="enable_tree_lines">True</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="history">
- <property name="height_request">30</property>
- <property name="visible">True</property>
- <property name="label" translatable="yes">History</property>
- </widget>
- <packing>
- <property name="position">1</property>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- <property name="type">tab</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="vbox3">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkHBox" id="hbox4">
- <property name="visible">True</property>
- <child>
- <widget class="GtkButton" id="messageTypeButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="messageStatusButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkScrolledWindow" id="message_scrolledwindow">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="hscrollbar_policy">never</property>
- <property name="vscrollbar_policy">automatic</property>
- <child>
- <widget class="GtkViewport" id="viewport1">
- <property name="visible">True</property>
- <property name="resize_mode">queue</property>
- <child>
- <widget class="GtkVBox" id="vbox4">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkTreeView" id="messages_view">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="rules_hint">True</property>
- <property name="enable_grid_lines">horizontal</property>
- <property name="enable_tree_lines">True</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="messages">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Messages</property>
- </widget>
- <packing>
- <property name="position">2</property>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- <property name="type">tab</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="vbox5">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkButton" id="addressbookSelectButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkScrolledWindow" id="contacts_scrolledwindow">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="hscrollbar_policy">never</property>
- <child>
- <widget class="GtkTreeView" id="contactsview">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="headers_visible">False</property>
- <property name="rules_hint">True</property>
- <property name="fixed_height_mode">True</property>
- <property name="enable_grid_lines">horizontal</property>
- <property name="enable_tree_lines">True</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="contacts">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Contacts</property>
- </widget>
- <packing>
- <property name="position">3</property>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- <property name="type">tab</property>
- </packing>
- </child>
- <child>
- <widget class="GtkTable" id="accountview">
- <property name="visible">True</property>
- <property name="border_width">11</property>
- <property name="n_rows">7</property>
- <property name="n_columns">2</property>
- <child>
- <widget class="GtkLabel" id="gcnumber_display">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">No Number Available</property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="gcnumber_label">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="ypad">10</property>
- <property name="label" translatable="yes">Account Number:</property>
- </widget>
- <packing>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="callback_number_label">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="ypad">10</property>
- <property name="label" translatable="yes">Callback Number:</property>
- </widget>
- <packing>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label4">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- </widget>
- <packing>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label5">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- </widget>
- <packing>
- <property name="top_attach">4</property>
- <property name="bottom_attach">5</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="vbox1">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkCheckButton" id="missedCheckbox">
- <property name="label" translatable="yes">Missed Calls</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="draw_indicator">True</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkCheckButton" id="voicemailCheckbox">
- <property name="label" translatable="yes">Voicemail</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="draw_indicator">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkCheckButton" id="smsCheckbox">
- <property name="label" translatable="yes">SMS</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="draw_indicator">True</property>
- </widget>
- <packing>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">4</property>
- <property name="bottom_attach">5</property>
- </packing>
- </child>
- <child>
- <widget class="GtkCheckButton" id="notifyCheckbox">
- <property name="label" translatable="yes">Notifications</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="yalign">0</property>
- <property name="draw_indicator">True</property>
- </widget>
- <packing>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="clearcookies">
- <property name="label" translatable="yes">New Login</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <property name="focus_on_click">False</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">6</property>
- <property name="bottom_attach">7</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label3">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- </widget>
- <packing>
- <property name="top_attach">5</property>
- <property name="bottom_attach">6</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="minutesEntryButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- <property name="y_options">GTK_FILL</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="callbackSelectButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <placeholder/>
- </child>
- <child>
- <placeholder/>
- </child>
- <child>
- <placeholder/>
- </child>
- </widget>
- <packing>
- <property name="position">4</property>
- <property name="tab_expand">True</property>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="account">
- <property name="height_request">30</property>
- <property name="visible">True</property>
- <property name="label" translatable="yes">Account</property>
- </widget>
- <packing>
- <property name="position">4</property>
- <property name="tab_fill">False</property>
- <property name="type">tab</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <widget class="GtkDialog" id="loginDialog">
- <property name="border_width">5</property>
- <property name="title" translatable="yes">Login</property>
- <property name="resizable">False</property>
- <property name="modal">True</property>
- <property name="window_position">center-on-parent</property>
- <property name="destroy_with_parent">True</property>
- <property name="type_hint">dialog</property>
- <property name="skip_taskbar_hint">True</property>
- <property name="skip_pager_hint">True</property>
- <property name="deletable">False</property>
- <property name="transient_for">mainWindow</property>
- <property name="has_separator">False</property>
- <child internal-child="vbox">
- <widget class="GtkVBox" id="loginLayout">
- <property name="visible">True</property>
- <property name="spacing">2</property>
- <child>
- <widget class="GtkComboBox" id="serviceCombo">
- <property name="visible">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkTable" id="table1">
- <property name="visible">True</property>
- <property name="n_rows">2</property>
- <property name="n_columns">2</property>
- <child>
- <widget class="GtkLabel" id="username_label">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Username</property>
- </widget>
- </child>
- <child>
- <widget class="GtkLabel" id="password_label">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Password</property>
- </widget>
- <packing>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkEntry" id="usernameentry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkEntry" id="passwordentry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="visibility">False</property>
- </widget>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">2</property>
- </packing>
- </child>
- <child internal-child="action_area">
- <widget class="GtkHButtonBox" id="dialog-action_area1">
- <property name="visible">True</property>
- <property name="layout_style">end</property>
- <child>
- <widget class="GtkButton" id="logins_close_button">
- <property name="label" translatable="yes">gtk-cancel</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <property name="use_stock">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="loginbutton">
- <property name="label" translatable="yes">gtk-ok</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="can_default">True</property>
- <property name="receives_default">True</property>
- <property name="use_stock">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="pack_type">end</property>
- <property name="position">0</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <widget class="GtkWindow" id="smsWindow">
- <property name="title" translatable="yes">SMS Entry</property>
- <child>
- <widget class="GtkVBox" id="smsLayout">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkEventBox" id="smsErrorEventBox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkHBox" id="smsErrorBox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="smsErrorImage">
- <property name="visible">True</property>
- <property name="stock">gtk-dialog-error</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="smsErrorDescription">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Unknown Error</property>
- <property name="use_markup">True</property>
- <property name="ellipsize">end</property>
- <property name="single_line_mode">True</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkImage" id="smsErrorClose">
- <property name="visible">True</property>
- <property name="stock">gtk-close</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="vbox1">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkScrolledWindow" id="smsMessages_scrolledwindow">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="hscrollbar_policy">automatic</property>
- <property name="vscrollbar_policy">automatic</property>
- <child>
- <widget class="GtkViewport" id="smsMessagesViewPort">
- <property name="visible">True</property>
- <property name="resize_mode">queue</property>
- <child>
- <widget class="GtkVBox" id="smsMessagesLayout">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkTreeView" id="smsMessages">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkHSeparator" id="hseparator1">
- <property name="visible">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkVBox" id="smsTargetList">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <property name="spacing">3</property>
- <child>
- <placeholder/>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkFrame" id="frame1">
- <property name="visible">True</property>
- <property name="border_width">5</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">none</property>
- <child>
- <widget class="GtkVBox" id="vbox2">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <widget class="GtkScrolledWindow" id="smsMessage_scrolledEntry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="hscrollbar_policy">never</property>
- <property name="vscrollbar_policy">automatic</property>
- <child>
- <widget class="GtkTextView" id="smsEntry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="wrap_mode">word</property>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- </widget>
- </child>
- <child>
- <widget class="GtkLabel" id="label3">
- <property name="label" translatable="yes"><b>frame1</b></property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="type">label_item</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">3</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkFrame" id="frame2">
- <property name="visible">True</property>
- <property name="border_width">5</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">none</property>
- <child>
- <widget class="GtkHBox" id="smsButtonLayout">
- <property name="visible">True</property>
- <property name="spacing">5</property>
- <child>
- <widget class="GtkHBox" id="smsCountBox">
- <property name="visible">True</property>
- <child>
- <widget class="GtkLabel" id="smsLetterCount1">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Letters:</property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="smsLetterCount">
- <property name="visible">True</property>
- <property name="label" translatable="yes">0</property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label5">
- <property name="visible">True</property>
- <property name="xalign">0.47999998927116394</property>
- <property name="yalign">0.49000000953674316</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="phoneTypeSelection">
- <property name="label" translatable="yes">No Phone Types Available</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </widget>
- <packing>
- <property name="fill">False</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label6">
- <property name="visible">True</property>
- <property name="xalign">0.47999998927116394</property>
- <property name="yalign">0.49000000953674316</property>
- </widget>
- <packing>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="sendSmsButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <child>
- <widget class="GtkHBox" id="hbox1">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="image1">
- <property name="visible">True</property>
- <property name="stock">gtk-select-font</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label2">
- <property name="visible">True</property>
- <property name="label" translatable="yes">SMS</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">4</property>
- </packing>
- </child>
- <child>
- <widget class="GtkButton" id="dialButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <child>
- <widget class="GtkHBox" id="hbox2">
- <property name="visible">True</property>
- <child>
- <widget class="GtkImage" id="image2">
- <property name="visible">True</property>
- <property name="stock">gtk-apply</property>
- </widget>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <widget class="GtkLabel" id="label4">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Dial</property>
- </widget>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">5</property>
- </packing>
- </child>
- </widget>
- </child>
- <child>
- <widget class="GtkLabel" id="label1">
- <property name="label" translatable="yes"><b>frame2</b></property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="type">label_item</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="expand">False</property>
- <property name="pack_type">end</property>
- <property name="position">2</property>
- </packing>
- </child>
- </widget>
- </child>
- </widget>
-</glade-interface>
-#!/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()
--- /dev/null
+#!/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)
+++ /dev/null
-#!/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'), (<type 'exceptions.RuntimeError'>, 'Goodbye'), (None, 'Meh'), (<type 'exceptions.GeneratorExit'>, 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()
+++ /dev/null
-#!/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("<span size='30000' weight='bold'>%s</span>" % (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("<span size='23000' weight='bold'>%s</span>" % (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 <i>(%s)</i> - %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 = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
- expandedMessages = [firstMessage]
- expandedMessages.extend(messages)
- if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
- firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
- secondMessage = "<i>%d Messages Hidden...</i>" % (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()
+++ /dev/null
-#!/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)
--- /dev/null
+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
+++ /dev/null
-#!/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)