From 5c1a2c066e4808aa85a5dabcf4616c38a7cec363 Mon Sep 17 00:00:00 2001 From: epage Date: Thu, 2 Oct 2008 23:05:32 +0000 Subject: [PATCH] Pulling in the latest of brontides manually git-svn-id: file:///svnroot/gc-dialer/branches/updatingSvn@151 c39d3808-3fe2-4d86-a59f-b7f623ee9f21 --- doc/changelog.Debian.gz | Bin 0 -> 264 bytes doc/changelog.gz | Bin 0 -> 257 bytes doc/copyright | 35 + src/dialcentral.desktop | 7 + src/dialcentral.py | 10 + src/dialcentral/browser_emu.py | 152 ++++ src/dialcentral/builddeb.py | 56 ++ src/dialcentral/evo_backend.py | 117 +++ src/dialcentral/gc_backend.py | 315 ++++++++ src/dialcentral/gc_dialer.glade | 847 ++++++++++++++++++++ src/dialcentral/gc_dialer.py | 855 +++++++++++++++++++++ src/icons/hicolor/26x26/hildon/dialcentral.png | Bin 0 -> 1671 bytes src/icons/hicolor/64x64/hildon/dialcentral.png | Bin 0 -> 6411 bytes src/icons/hicolor/scalable/hildon/dialcentral.png | Bin 0 -> 32182 bytes support/DEBIAN/control | 40 + support/DEBIAN/postinst | 3 + 16 files changed, 2437 insertions(+) create mode 100644 doc/changelog.Debian.gz create mode 100644 doc/changelog.gz create mode 100644 doc/copyright create mode 100644 src/dialcentral.desktop create mode 100755 src/dialcentral.py create mode 100644 src/dialcentral/__init__.py create mode 100644 src/dialcentral/browser_emu.py create mode 100755 src/dialcentral/builddeb.py create mode 100644 src/dialcentral/evo_backend.py create mode 100644 src/dialcentral/gc_backend.py create mode 100644 src/dialcentral/gc_dialer.glade create mode 100755 src/dialcentral/gc_dialer.py create mode 100644 src/icons/hicolor/26x26/hildon/dialcentral.png create mode 100644 src/icons/hicolor/64x64/hildon/dialcentral.png create mode 100644 src/icons/hicolor/scalable/hildon/dialcentral.png create mode 100644 support/DEBIAN/control create mode 100755 support/DEBIAN/postinst diff --git a/doc/changelog.Debian.gz b/doc/changelog.Debian.gz new file mode 100644 index 0000000000000000000000000000000000000000..3cd100e571e1fe578b90b7163e74a51d98f3a5ec GIT binary patch literal 264 zcmV+j0r&nNiwFo|p}a@}17m1mZf9j|Z)Yw?C4j{fGc;^E}$H|l;Zkl>uH`U=5*{F^Dgh@M2 z?)nmCUaf$CjtcA37z~s=Mu{O`)`j5?@7^ot+=}$b(>RLS0_>tpo;?!VEZT?E;Vg=l z%~%mqQF0c3f?b!3Pl+C0=7g494->e^q)5}yvM!#Jl?0?7)XHS=rHOP48Yo>ch%)j( zF_s_a@XNV$s49GDaQG%oJ>!|n!t+}

8jY;T8Cj^nhlA6OY(7%^utBakD!i%*FjUgm_BTn`ht$fQWq(6TO`la&Od9@NTY@TG}#3mPb0F^DqqKrxmd=kUw9 zbf_wPXmI!@O+Dk8%fj on +Mon, 01 Sep 2008 22:13:53 +0000. + +It was downloaded from + +Upstream Author: Eric Warnke + +Copyright: 2008 by Eric Warnke + +License: + + + This package 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 of the License, or (at your option) any later version. + + This package 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 package; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +On Debian systems, the complete text of the GNU Lesser General +Public License can be found in `/usr/share/common-licenses/LGPL'. + + +The Debian packaging is (C) 2008, Eric Warnke and +is licensed under the GPL, see above. + +# Please also look if there are files or directories which have a +# different copyright/license attached and list them here. diff --git a/src/dialcentral.desktop b/src/dialcentral.desktop new file mode 100644 index 0000000..a3df1b0 --- /dev/null +++ b/src/dialcentral.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Encoding=UTF-8 +Version=1.0 +Type=Application +Name=DialCentral +Exec=/usr/bin/dialcentral.py +Icon=dialcentral diff --git a/src/dialcentral.py b/src/dialcentral.py new file mode 100755 index 0000000..f73c038 --- /dev/null +++ b/src/dialcentral.py @@ -0,0 +1,10 @@ +#!/usr/bin/python + +import sys + +sys.path.insert(0,"/usr/lib/dialcentral/") + +from gc_dialer import run_dialpad + +run_dialpad() + diff --git a/src/dialcentral/__init__.py b/src/dialcentral/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/dialcentral/browser_emu.py b/src/dialcentral/browser_emu.py new file mode 100644 index 0000000..3083a1c --- /dev/null +++ b/src/dialcentral/browser_emu.py @@ -0,0 +1,152 @@ +""" +@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 + - caching + - configurable user agent string + - GET and POST + - multipart POST (send files) + - receive content into file + - progress indicator + +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: + + - For testing, use a MozillaCacher instance - this will cache all pages and make testing quicker + - 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 warnings + + +class MozillaEmulator(object): + + def __init__(self, cacher=None, trycount=0): + """Create a new MozillaEmulator object. + + @param cacher: A dictionary like object, that can cache search results on a storage device. + You can use a simple dictionary here, but it is not recommended. + You can also put None here to disable caching completely. + @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.""" + if cacher is None: + cacher = {} + self.cacher = cacher + self.cookies = cookielib.LWPCookieJar() + self.debug = False + self.trycount = trycount + + def build_opener(self, url, postdata=None, extraheaders=None, forbid_redirect=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', + } + for key, value in extraheaders.iteritems(): + txheaders[key] = value + req = urllib2.Request(url, postdata, txheaders) + self.cookies.add_cookie_header(req) + if forbid_redirect: + redirector = HTTPNoRedirector() + else: + redirector = urllib2.HTTPRedirectHandler() + + 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 + ) + u.addheaders = [( + 'User-Agent', + 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.8) Gecko/20050511 Firefox/1.0.4' + )] + if not postdata is None: + req.add_data(postdata) + return (req, u) + + def download(self, url, postdata=None, extraheaders=None, forbid_redirect=False, + trycount=None, fd=None, onprogress=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 forbid_redirect: 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 fd: You can pass a file descriptor here. In this case, + the data will be written into the file. Please note that + when you save the raw data into a file then it won't be cached. + @param onprogress: A function that has two parameters: + the size of the resource and the downloaded size. This will be + called for each 1KB chunk. (If the HTTP header does not contain + the content-length field, then the size parameter will be zero!) + @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, unless fd was specified. When fd + was given, the return value is undefined. + """ + warnings.warn("Performing download of %s" % url, UserWarning, 2) + + 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, forbid_redirect) + openerdirector = u.open(req) + if self.debug: + print req.get_method(), url + print openerdirector.code, openerdirector.msg + print openerdirector.headers + self.cookies.extract_cookies(openerdirector, req) + if only_head: + return openerdirector + return openerdirector.read() + except urllib2.URLError: + cnt += 1 + if (trycount > -1) and (trycount < cnt): + raise + # Retry :-) + if self.debug: + print "MozillaEmulator: urllib2.URLError, retryting ", cnt + + +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 + raise e diff --git a/src/dialcentral/builddeb.py b/src/dialcentral/builddeb.py new file mode 100755 index 0000000..027c059 --- /dev/null +++ b/src/dialcentral/builddeb.py @@ -0,0 +1,56 @@ +#!/usr/bin/python2.5 + +from py2deb import * + +__appname__ = "dialcentral" +__description__ = "Simple interface to Google's GrandCentral(tm) service" +__author__ = "Eric Warnke" +__email__ = "ericew@gmail.com" +__version__ = "0.8.0" +__build__ = 9 +__changelog__ = '''\ +0.8.0 - "Spit and polish" + * Addressbook support + * threaded networking for better interactivity + * Hold down back to clear number + * Standard about dialog + * many more smaller fixes +''' + +__postinstall__ = '''#!/bin/sh + +gtk-update-icon-cache /usr/share/icons/hicolor +''' + + +if __name__ == "__main__": + try: + os.chdir(os.path.dirname(sys.argv[0])) + except: + pass + + + p=Py2deb(__appname__) + p.description=__description__ + p.author=__author__ + p.mail=__email__ + p.license = "lgpl" + p.depends = "python2.5, python2.5-gtk2" + p.section="user/communication" + p.arch="all" + p.urgency="low" + p.distribution="chinook diablo" + p.repository="extras-devel" + p.changelog=__changelog__ + p.postinstall=__postinstall__ + p.icon="26x26-dialcentral.png" + p["/usr/bin"] = [ "dialcentral.py" ] + p["/usr/lib/dialcentral"] = ["__init__.py", "browser_emu.py", "evo_backend.py", "gc_backend.py", "gc_dialer.glade", "gc_dialer.py", "builddeb.py"] + p["/usr/share/applications/hildon"] = ["dialcentral.desktop"] + p["/usr/share/icons/hicolor/26x26/hildon"] = ["26x26-dialcentral.png|dialcentral.png"] + p["/usr/share/icons/hicolor/64x64/hildon"] = ["64x64-dialcentral.png|dialcentral.png"] + p["/usr/share/icons/hicolor/scalable/hildon"] = ["scale-dialcentral.png|dialcentral.png"] + + print p + print p.generate(__version__,__build__,changelog=__changelog__,tar=True,dsc=True,changes=True,build=False,src=True) + diff --git a/src/dialcentral/evo_backend.py b/src/dialcentral/evo_backend.py new file mode 100644 index 0000000..c094c2e --- /dev/null +++ b/src/dialcentral/evo_backend.py @@ -0,0 +1,117 @@ +#!/usr/bin/python + +# GC Dialer - Front end for Google's Grand Central 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 + + +""" +Evolution Contact Support +""" + + +try: + import evolution +except ImportError: + evolution = None + + +class EvolutionAddressBook(object): + """ + @note Combined the factory and the addressbook for "simplicity" and "cutting down" the number of allocations/deallocations + """ + + def __init__(self, bookId = None): + if not self.is_supported(): + return + + self._phoneTypes = None + self._bookId = bookId if bookId is not None else self.get_addressbooks().next()[1] + self._book = evolution.ebook.open_addressbook(self._bookId) + + @classmethod + def is_supported(cls): + return evolution is not None + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + if not self.is_supported(): + return + + if len(evolution.ebook.list_addressbooks()) == 0 and evolution.ebook.open_addressbook('default') is not None: + # It appears that Maemo's e-d-s does not always list the default addressbook, so we're faking it being listed + yield self, "default", "Maemo" + + for bookId in evolution.ebook.list_addressbooks(): + yield self, bookId[1], bookId[0] + + def open_addressbook(self, bookId): + self._bookId = bookId + self._book = evolution.ebook.open_addressbook(self._bookId) + return self + + @staticmethod + def factory_short_name(): + return "Evo" + + @staticmethod + def factory_name(): + return "Evolution" + + def get_contacts(self): + """ + @returns Iterable of (contact id, contact name) + """ + if not self.is_supported(): + return + + for contact in self._book.get_all_contacts(): + yield contact.get_uid(), contact.props.full_name + + def get_contact_details(self, contactId): + """ + @returns Iterable of (Phone Type, Phone Number) + """ + contact = self._book.get_contact(contactId) + + if self._phoneTypes is None and contact is not None: + self._phoneTypes = [pt for pt in dir(contact.props) if "phone" in pt.lower()] + + for phoneType in self._phoneTypes: + phoneNumber = getattr(contact.props, phoneType) + if isinstance(phoneNumber, str): + yield phoneType, phoneNumber + +def print_addressbooks(): + """ + Included here for debugging. + + Either insert it into the code or launch python with the "-i" flag + """ + if not EvolutionAddressBook.is_supported(): + print "No Evolution Support" + return + + eab = EvolutionAddressBook() + for book in eab.get_addressbooks(): + eab = eab.open_addressbook(book[1]) + print book + for contact in eab.get_contacts(): + print "\t", contact + for details in eab.get_contact_details(contact[0]): + print "\t\t", details diff --git a/src/dialcentral/gc_backend.py b/src/dialcentral/gc_backend.py new file mode 100644 index 0000000..d1641de --- /dev/null +++ b/src/dialcentral/gc_backend.py @@ -0,0 +1,315 @@ +#!/usr/bin/python + +# GC Dialer - Front end for Google's Grand Central 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 + +""" +Grandcentral Dialer backend code +""" + + +import os +import re +import urllib +import urllib2 +import time +import warnings + +from browser_emu import MozillaEmulator + + +class GCDialer(object): + """ + This class encapsulates all of the knowledge necessary to interace with the grandcentral servers + the functions include login, setting up a callback number, and initalting a callback + """ + + _gcDialingStrRe = re.compile("This may take a few seconds", re.M) + _accessTokenRe = re.compile(r"""]*value="(.*)"/>""") + _isLoginPageRe = re.compile(r"""

""") + _callbackRe = re.compile(r"""name="default_number" value="(\d+)" />\s+(.*)\s$""", re.M) + _accountNumRe = re.compile(r"""GrandCentral\s*(.{14})\s* """, re.M) + _inboxRe = re.compile(r""".*?(voicemail|received|missed|call return).*?\s+\s+\s+(.*?)\s+ \| \s+(.*?)\s?\s+
\s+(.*?)\s?(.*?)""", re.S) + _contactsNextRe = re.compile(r""".*Next""", re.S) + _contactDetailGroupRe = re.compile(r"""Group:\s*(\w*)""", re.S) + _contactDetailPhoneRe = re.compile(r"""(\w+):[0-9\-\(\) \t]*?call""", re.S) + + _validateRe = re.compile("^[0-9]{10,}$") + + _forwardselectURL = "http://www.grandcentral.com/mobile/settings/forwarding_select" + _loginURL = "https://www.grandcentral.com/mobile/account/login" + _setforwardURL = "http://www.grandcentral.com/mobile/settings/set_forwarding?from=settings" + _clicktocallURL = "http://www.grandcentral.com/mobile/calls/click_to_call?a_t=%s&destno=%s" + _inboxallURL = "http://www.grandcentral.com/mobile/messages/inbox?types=all" + _contactsURL = "http://www.grandcentral.com/mobile/contacts" + _contactDetailURL = "http://www.grandcentral.com/mobile/contacts/detail" + + def __init__(self, cookieFile = None): + # Important items in this function are the setup of the browser emulation and cookie file + self._msg = "" + + self._browser = MozillaEmulator(None, 0) + if cookieFile is None: + cookieFile = os.path.join(os.path.expanduser("~"), ".gc_dialer_cookies.txt") + self._browser.cookies.filename = cookieFile + if os.path.isfile(cookieFile): + self._browser.cookies.load() + + self._accessToken = None + self._accountNum = None + self._callbackNumbers = {} + self._lastAuthed = 0.0 + + def is_authed(self, force = False): + """ + Attempts to detect a current session and pull the auth token ( a_t ) from the page. + @note Once logged in try not to reauth more than once a minute. + @returns If authenticated + """ + + if (time.time() - self._lastAuthed) < 60 and not force: + return True + + try: + forwardSelectionPage = self._browser.download(GCDialer._forwardselectURL) + except urllib2.URLError, e: + warnings.warn("%s is not accesible" % GCDialer._forwardselectURL, UserWarning, 2) + return False + + self._browser.cookies.save() + if GCDialer._isLoginPageRe.search(forwardSelectionPage) is None: + self._grab_token(forwardSelectionPage) + self._lastAuthed = time.time() + return True + + return False + + def login(self, username, password): + """ + Attempt to login to grandcentral + @returns Whether login was successful or not + """ + if self.is_authed(): + return True + + loginPostData = urllib.urlencode( {'username' : username , 'password' : password } ) + + try: + loginSuccessOrFailurePage = self._browser.download(GCDialer._loginURL, loginPostData) + except urllib2.URLError, e: + warnings.warn("%s is not accesible" % GCDialer._loginURL, UserWarning, 2) + return False + + return self.is_authed() + + def logout(self): + self._lastAuthed = 0.0 + self._browser.cookies.clear() + self._browser.cookies.save() + + def dial(self, number): + """ + This is the main function responsible for initating the callback + """ + self._msg = "" + + # If the number is not valid throw exception + if not self.is_valid_syntax(number): + raise ValueError('number is not valid') + + # No point if we don't have the magic cookie + if not self.is_authed(): + self._msg = "Not authenticated" + return False + + # Strip leading 1 from 11 digit dialing + if len(number) == 11 and number[0] == 1: + number = number[1:] + + try: + callSuccessPage = self._browser.download( + GCDialer._clicktocallURL % (self._accessToken, number), + None, + {'Referer' : 'http://www.grandcentral.com/mobile/messages'} + ) + except urllib2.URLError, e: + warnings.warn("%s is not accesible" % GCDialer._clicktocallURL, UserWarning, 2) + return False + + if GCDialer._gcDialingStrRe.search(callSuccessPage) is not None: + return True + else: + self._msg = "Grand Central returned an error" + return False + + self._msg = "Unknown Error" + return False + + def clear_caches(self): + pass + + 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 grand central phone number + """ + return self._accountNum + + def set_sane_callback(self): + """ + 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 = self.get_callback_numbers() + + for number, description in numbers.iteritems(): + if not re.compile(r"""1747""").match(number) is None: + self.set_callback_number(number) + return + + for number, description in numbers.iteritems(): + if not re.compile(r"""gizmo""", re.I).search(description) is None: + self.set_callback_number(number) + return + + for number, description in numbers.iteritems(): + if not re.compile(r"""computer""", re.I).search(description) is None: + self.set_callback_number(number) + return + + for number, description in numbers.iteritems(): + self.set_callback_number(number) + return + + def get_callback_numbers(self): + """ + @returns a dictionary mapping call back numbers to descriptions + @note These results are cached for 30 minutes. + """ + if time.time() - self._lastAuthed < 1800 or self.is_authed(): + return self._callbackNumbers + + return {} + + def set_callback_number(self, callbacknumber): + """ + Set the number that grandcental calls + @param callbacknumber should be a proper 10 digit number + """ + callbackPostData = urllib.urlencode({ + 'a_t': self._accessToken, + 'default_number': callbacknumber + }) + try: + callbackSetPage = self._browser.download(GCDialer._setforwardURL, callbackPostData) + except urllib2.URLError, e: + warnings.warn("%s is not accesible" % GCDialer._setforwardURL, UserWarning, 2) + return False + + self._browser.cookies.save() + return True + + def get_callback_number(self): + """ + @returns Current callback number or None + """ + for c in self._browser.cookies: + if c.name == "pda_forwarding_number": + return c.value + return None + + def get_recent(self): + """ + @returns Iterable of (personsName, phoneNumber, date, action) + """ + try: + recentCallsPage = self._browser.download(GCDialer._inboxallURL) + except urllib2.URLError, e: + warnings.warn("%s is not accesible" % GCDialer._inboxallURL, UserWarning, 2) + return + + for match in self._inboxRe.finditer(recentCallsPage): + phoneNumber = match.group(4) + action = match.group(1) + date = match.group(2) + personsName = match.group(3) + yield personsName, phoneNumber, date, action + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + yield self, "", "" + + def open_addressbook(self, bookId): + return self + + @staticmethod + def factory_short_name(): + return "GC" + + @staticmethod + def factory_name(): + return "Grand Central" + + def get_contacts(self): + """ + @returns Iterable of (contact id, contact name) + """ + contactsPagesUrls = [GCDialer._contactsURL] + for contactsPageUrl in contactsPagesUrls: + contactsPage = self._browser.download(contactsPageUrl) + for contact_match in self._contactsRe.finditer(contactsPage): + contactId = contact_match.group(1) + contactName = contact_match.group(2) + yield contactId, contactName + + next_match = self._contactsNextRe.match(contactsPage) + if next_match is not None: + newContactsPageUrl = self._contactsURL + next_match.group(1) + contactsPagesUrls.append(newContactsPageUrl) + + def get_contact_details(self, contactId): + """ + @returns Iterable of (Phone Type, Phone Number) + """ + detailPage = self._browser.download(GCDialer._contactDetailURL + '/' + contactId) + for detail_match in self._contactDetailPhoneRe.finditer(detailPage): + phoneType = detail_match.group(1) + phoneNumber = detail_match.group(2) + yield (phoneType, phoneNumber) + + def _grab_token(self, data): + "Pull the magic cookie from the datastream" + atGroup = GCDialer._accessTokenRe.search(data) + self._accessToken = atGroup.group(1) + + anGroup = GCDialer._accountNumRe.search(data) + self._accountNum = anGroup.group(1) + + self._callbackNumbers = {} + for match in GCDialer._callbackRe.finditer(data): + self._callbackNumbers[match.group(1)] = match.group(2) diff --git a/src/dialcentral/gc_dialer.glade b/src/dialcentral/gc_dialer.glade new file mode 100644 index 0000000..4c9b830 --- /dev/null +++ b/src/dialcentral/gc_dialer.glade @@ -0,0 +1,847 @@ + + + + + + 400 + 350 + Dialer + + + True + + + True + + + True + _File + True + + + True + + + True + _New Login + True + + + + gtk-new + + + + + + + True + + + + + True + gtk-quit + True + True + + + + + + + + + + True + _Edit + True + + + True + + + True + gtk-paste + True + True + + + + + + True + gtk-delete + True + True + + + + + + + + + + True + _About + True + + + + + + False + + + + + True + True + GTK_POS_BOTTOM + False + True + + + + True + + + 50 + True + <span size="35000" weight="bold">(518) 555-1212</span> + True + GTK_JUSTIFY_CENTER + + + False + False + + + + + True + 4 + 3 + True + + + True + True + 0 + + + + + True + + + True + 1 + gtk-yes + + + + + True + 0 + 5 + <span size="17000" weight="bold">Dial</span> + True + + + 1 + + + + + + + 2 + 3 + 3 + 4 + + + + + True + False + 0 + + + + + True + <span size="33000" weight="bold">0</span> +<span size="9000"></span> + True + GTK_JUSTIFY_CENTER + + + + + 1 + 2 + 3 + 4 + + + + + True + False + 0 + + + + + + + True + + + True + 1 + gtk-no + + + + + True + 0 + 5 + <span size="17000" weight="Bold">Back</span> + True + + + 1 + + + + + + + 3 + 4 + + + + + True + False + 0 + + + + + + + + + True + <span size="30000" weight="bold">9</span> +<span size="12000">WXYZ</span> + True + GTK_JUSTIFY_CENTER + + + + + 2 + 3 + 2 + 3 + + + + + True + False + 0 + + + + + + + + True + <span size="30000" weight="bold">8</span> +<span size="12000">TUV</span> + True + GTK_JUSTIFY_CENTER + + + + + 1 + 2 + 2 + 3 + + + + + True + False + 0 + + + + + + + + + True + <span size="30000" weight="bold">7</span> +<span size="12000">PQRS</span> + True + GTK_JUSTIFY_CENTER + + + + + 2 + 3 + + + + + True + False + 0 + + + + + + + + True + <span size="30000" weight="bold">6</span> +<span size="12000">MNO</span> + True + GTK_JUSTIFY_CENTER + + + + + 2 + 3 + 1 + 2 + + + + + True + False + 0 + + + + + + + + True + <span size="30000" weight="bold">5</span> +<span size="12000">JKL</span> + True + GTK_JUSTIFY_CENTER + + + + + 1 + 2 + 1 + 2 + + + + + True + False + 0 + + + + + + + + True + <span size="30000" weight="bold">4</span> +<span size="12000">GHI</span> + True + GTK_JUSTIFY_CENTER + + + + + 1 + 2 + + + + + True + False + 0 + + + + + + + + True + <span size="30000" weight="bold" stretch="ultraexpanded">3</span> +<span size="12000">DEF</span> + True + GTK_JUSTIFY_CENTER + + + + + 2 + 3 + + + + + True + False + 0 + + + + + + + + True + <span size="30000" weight="bold">2</span> +<span size="12000">ABC</span> + True + GTK_JUSTIFY_CENTER + + + + + 1 + 2 + + + + + True + False + 0 + + + + + True + <span size="33000" weight="bold">1</span> +<span size="9000"> </span> + True + + + + + + + 1 + + + + + True + False + + + + + 30 + True + Keypad + + + tab + True + False + + + + + True + 2 + 1 + + + True + True + GTK_POLICY_NEVER + + + True + True + False + False + True + GTK_TREE_VIEW_GRID_LINES_HORIZONTAL + True + + + + + + 1 + 2 + + + + + True + + + + GTK_FILL + + + + + 1 + True + False + + + + + True + Contacts + + + tab + 1 + True + False + + + + + True + 11 + 3 + 2 + + + + + + True + 1 + 5 + GrandCentral +Number: + GTK_JUSTIFY_RIGHT + + + + + True + <span size="15000" weight="bold">(518) 555-1212</span> + True + + + 1 + 2 + + + + + + True + True + True + Clear Account Information +must reauthenticate + 0 + + + + 1 + 2 + 1 + 2 + + + + + + True + 1 + 5 + Callback Number: + + + 2 + 3 + + + + + True + + + True + True + + + + + + 1 + 2 + 2 + 3 + GTK_FILL + + + + + + 2 + True + False + + + + + 30 + True + Account + + + tab + 2 + False + + + + + True + True + GTK_POLICY_NEVER + + + True + True + False + False + True + GTK_TREE_VIEW_GRID_LINES_HORIZONTAL + True + + + + + + 1 + True + False + + + + + 30 + True + Recent + + + tab + 1 + True + False + + + + + 1 + + + + + + + 5 + Grandcentral Login + False + True + GTK_WIN_POS_CENTER_ON_PARENT + True + GDK_WINDOW_TYPE_HINT_DIALOG + True + True + False + Dialpad + False + + + True + 2 + + + True + 2 + 2 + + + True + Username + + + + + True + Password + + + 1 + 2 + + + + + True + True + + + 1 + 2 + + + + + True + True + False + + + 1 + 2 + 1 + 2 + + + + + 1 + + + + + True + GTK_BUTTONBOX_END + + + True + True + True + True + True + Login + 0 + + + + + + True + True + True + Close + 0 + + + + 1 + + + + + False + GTK_PACK_END + + + + + + + 5 + Select Phone Type + False + True + GTK_WIN_POS_CENTER_ON_PARENT + True + GDK_WINDOW_TYPE_HINT_DIALOG + True + True + False + Dialpad + False + + + True + 2 + + + True + True + True + + + + 1 + + + + + True + GTK_BUTTONBOX_END + + + True + True + Select + 0 + + + + + + True + True + True + Cancel + 0 + + + + 1 + + + + + False + GTK_PACK_END + + + + + + diff --git a/src/dialcentral/gc_dialer.py b/src/dialcentral/gc_dialer.py new file mode 100755 index 0000000..b3ce552 --- /dev/null +++ b/src/dialcentral/gc_dialer.py @@ -0,0 +1,855 @@ +#!/usr/bin/python2.5 + +# GC Dialer - 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 + + +""" +Grandcentral Dialer +""" + +import sys +import gc +import os +import threading +import time +import warnings + +import gobject +import gtk +gtk.gdk.threads_init() +import gtk.glade + +try: + import hildon +except ImportError: + hildon = None + + +""" +This changes the default, system wide, socket timeout so that a hung server will not completly +hork the application +""" +import socket +socket.setdefaulttimeout(5) + + +def make_ugly(prettynumber): + """ + function to take a phone number and strip out all non-numeric + characters + + >>> make_ugly("+012-(345)-678-90") + '01234567890' + """ + import re + uglynumber = re.sub('\D', '', prettynumber) + return uglynumber + + +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") + '(234)-567-8901' + >>> make_pretty("12345678901") + '1 (234)-567-8901' + >>> make_pretty("01234567890") + '+012-(345)-678-90' + """ + if phonenumber is None or phonenumber is "": + return "" + + if len(phonenumber) < 3: + return phonenumber + + if phonenumber[0] == "0": + prettynumber = "" + prettynumber += "+%s" % phonenumber[0:3] + if 3 < len(phonenumber): + prettynumber += "-(%s)" % phonenumber[3:6] + if 6 < len(phonenumber): + prettynumber += "-%s" % phonenumber[6:9] + if 9 < len(phonenumber): + prettynumber += "-%s" % phonenumber[9:] + return prettynumber + elif len(phonenumber) <= 7: + prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:]) + elif len(phonenumber) > 8 and phonenumber[0] == "1": + prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:]) + elif len(phonenumber) > 7: + prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:]) + return prettynumber + + +def make_idler(func): + """ + Decorator that makes a generator-function into a function that will continue execution on next call + """ + a = [] + + 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 + + decorated_func.__name__ = func.__name__ + decorated_func.__doc__ = func.__doc__ + decorated_func.__dict__.update(func.__dict__) + + return decorated_func + + +class DummyAddressBook(object): + """ + Minimal example of both an addressbook factory and an addressbook + """ + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + yield self, "", "None" + + def open_addressbook(self, bookId): + return self + + @staticmethod + def factory_short_name(): + return "" + + @staticmethod + def factory_name(): + return "" + + @staticmethod + def get_contacts(): + """ + @returns Iterable of (contact id, contact name) + """ + return [] + + @staticmethod + def get_contact_details(contactId): + """ + @returns Iterable of (Phone Type, Phone Number) + """ + return [] + + +class PhoneTypeSelector(object): + + def __init__(self, widgetTree, gcBackend): + self._gcBackend = gcBackend + self._widgetTree = widgetTree + self._dialog = self._widgetTree.get_widget("phonetype_dialog") + + self._selectButton = self._widgetTree.get_widget("select_button") + self._selectButton.connect("clicked", self._on_phonetype_select) + + self._cancelButton = self._widgetTree.get_widget("cancel_button") + self._cancelButton.connect("clicked", self._on_phonetype_cancel) + + self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + self._typeviewselection = None + + typeview = self._widgetTree.get_widget("phonetypes") + typeview.connect("row-activated", self._on_phonetype_select) + typeview.set_model(self._typemodel) + textrenderer = gtk.CellRendererText() + + # Add the column to the treeview + column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1) + column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + + typeview.append_column(column) + + self._typeviewselection = typeview.get_selection() + self._typeviewselection.set_mode(gtk.SELECTION_SINGLE) + + def run(self, contactDetails): + self._typemodel.clear() + + for phoneType, phoneNumber in contactDetails: + self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType))) + + userResponse = self._dialog.run() + + if userResponse == gtk.RESPONSE_OK: + model, itr = self._typeviewselection.get_selected() + if itr: + phoneNumber = self._typemodel.get_value(itr, 0) + else: + phoneNumber = "" + + self._typeviewselection.unselect_all() + self._dialog.hide() + return phoneNumber + + def _on_phonetype_select(self, *args): + self._dialog.response(gtk.RESPONSE_OK) + + def _on_phonetype_cancel(self, *args): + self._dialog.response(gtk.RESPONSE_CANCEL) + + +class Dialpad(object): + + __pretty_app_name__ = "DialCentral" + __app_name__ = "dialcentral" + __version__ = "0.8.0" + __app_magic__ = 0xdeadbeef + + _glade_files = [ + './gc_dialer.glade', + '../lib/gc_dialer.glade', + '/usr/lib/dialcentral/gc_dialer.glade', + ] + + def __init__(self): + self._phonenumber = "" + self._prettynumber = "" + self._areacode = "518" + + self._clipboard = gtk.clipboard_get() + + self._deviceIsOnline = True + self._callbackList = None + + self._recenttime = 0.0 + self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + self._recentviewselection = None + + self._contactstime = 0.0 + self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) + self._contactsviewselection = None + + self._clearall_id = None + + for path in Dialpad._glade_files: + if os.path.isfile(path): + self._widgetTree = gtk.glade.XML(path) + break + else: + self.display_error_message("Cannot find gc_dialer.glade") + gtk.main_quit() + return + + #Get the buffer associated with the number display + self._numberdisplay = self._widgetTree.get_widget("numberdisplay") + self.set_number("") + self._notebook = self._widgetTree.get_widget("notebook") + + self._window = self._widgetTree.get_widget("Dialpad") + + global hildon + self._app = None + self._isFullScreen = False + if hildon is not None and self._window is gtk.Window: + warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2) + hildon = None + elif hildon is not None: + self._app = hildon.Program() + self._window = hildon.Window() + self._widgetTree.get_widget("vbox1").reparent(self._window) + self._app.add_window(self._window) + self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4)) + self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7) + self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29)) + + gtkMenu = self._widgetTree.get_widget("dialpad_menubar") + menu = gtk.Menu() + for child in gtkMenu.get_children(): + child.reparent(menu) + self._window.set_menu(menu) + gtkMenu.destroy() + + self._window.connect("key-press-event", self._on_key_press) + self._window.connect("window-state-event", self._on_window_state_change) + else: + warnings.warn("No Hildon", UserWarning, 2) + + if hildon is not None: + self._window.set_title("Keypad") + else: + self._window.set_title("%s - Keypad" % self.__pretty_app_name__) + + callbackMapping = { + # Process signals from buttons + "on_loginbutton_clicked": self._on_loginbutton_clicked, + "on_loginclose_clicked": self._on_loginclose_clicked, + + "on_dialpad_quit": self._on_close, + "on_paste": self._on_paste, + "on_clear_number": self._on_clear_number, + + "on_clearcookies_clicked": self._on_clearcookies_clicked, + "on_notebook_switch_page": self._on_notebook_switch_page, + "on_recentview_row_activated": self._on_recentview_row_activated, + "on_contactsview_row_activated" : self._on_contactsview_row_activated, + + "on_digit_clicked": self._on_digit_clicked, + "on_back_clicked": self._on_backspace, + "on_dial_clicked": self._on_dial_clicked, + "on_back_pressed": self._on_back_pressed, + "on_back_released": self._on_back_released, + "on_addressbook_combo_changed": self._on_addressbook_combo_changed, + "on_about_activate": self._on_about_activate, + } + self._widgetTree.signal_autoconnect(callbackMapping) + self._widgetTree.get_widget("callbackcombo").get_child().connect("changed", self._on_callbackentry_changed) + + if self._window: + self._window.connect("destroy", gtk.main_quit) + self._window.show_all() + + self.set_account_number("") + self._widgetTree.get_widget("dial").grab_default() + self._widgetTree.get_widget("dial").grab_focus() + + threading.Thread(target=self._idle_setup).start() + + + def _idle_setup(self): + """ + If something can be done after the UI loads, push it here so it's not blocking the UI + """ + + from gc_backend import GCDialer + from evo_backend import EvolutionAddressBook + + self._gcBackend = GCDialer() + + try: + import osso + except ImportError: + osso = None + + try: + import conic + except ImportError: + conic = None + + + self._osso = None + if osso is not None: + self._osso = osso.Context(Dialpad.__app_name__, Dialpad.__version__, False) + device = osso.DeviceState(self._osso) + device.set_device_state_callback(self._on_device_state_change, 0) + else: + warnings.warn("No OSSO", UserWarning, 2) + + self._connection = None + if conic is not None: + self._connection = conic.Connection() + self._connection.connect("connection-event", self._on_connection_change, Dialpad.__app_magic__) + self._connection.request_connection(conic.CONNECT_FLAG_NONE) + else: + warnings.warn("No Internet Connectivity API ", UserWarning, 2) + + + self._addressBookFactories = [ + self._gcBackend, + DummyAddressBook(), + EvolutionAddressBook(), + ] + self._addressBook = None + self.open_addressbook(*self.get_addressbooks().next()[0][0:2]) + + gtk.gdk.threads_enter() + self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) + gtk.gdk.threads_leave() + + 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) + gtk.gdk.threads_enter() + self._booksList.append(row) + gtk.gdk.threads_leave() + + gtk.gdk.threads_enter() + combobox = self._widgetTree.get_widget("addressbook_combo") + combobox.set_model(self._booksList) + cell = gtk.CellRendererText() + combobox.pack_start(cell, True) + combobox.add_attribute(cell, 'text', 2) + combobox.set_active(0) + gtk.gdk.threads_leave() + + self._phoneTypeSelector = PhoneTypeSelector(self._widgetTree, self._gcBackend) + + gtk.gdk.threads_enter() + self._init_recent_view() + self._init_contacts_view() + gtk.gdk.threads_leave() + + """ + This is where the blocking can start + """ + if self._gcBackend.is_authed(): + gtk.gdk.threads_enter() + self.set_account_number(self._gcBackend.get_account_number()) + self.populate_callback_combo() + gtk.gdk.threads_leave() + else: + self.attempt_login(2) + + return False + + def _init_recent_view(self): + recentview = self._widgetTree.get_widget("recentview") + recentview.set_model(self._recentmodel) + textrenderer = gtk.CellRendererText() + + # Add the column to the treeview + column = gtk.TreeViewColumn("Calls", textrenderer, text=1) + column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + + recentview.append_column(column) + + self._recentviewselection = recentview.get_selection() + self._recentviewselection.set_mode(gtk.SELECTION_SINGLE) + + return False + + def _init_contacts_view(self): + contactsview = self._widgetTree.get_widget("contactsview") + contactsview.set_model(self._contactsmodel) + + # Add the column to the treeview + column = gtk.TreeViewColumn("Contact") + + textrenderer = gtk.CellRendererText() + column.pack_start(textrenderer, expand=True) + column.add_attribute(textrenderer, 'text', 1) + + textrenderer = gtk.CellRendererText() + column.pack_start(textrenderer, expand=True) + column.add_attribute(textrenderer, 'text', 4) + + column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + column.set_sort_column_id(1) + column.set_visible(True) + contactsview.append_column(column) + + #textrenderer = gtk.CellRendererText() + #column = gtk.TreeViewColumn("Location", textrenderer, text=2) + #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + #column.set_sort_column_id(2) + #column.set_visible(True) + #contactsview.append_column(column) + + #textrenderer = gtk.CellRendererText() + #column = gtk.TreeViewColumn("Phone", textrenderer, text=3) + #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + #column.set_sort_column_id(3) + #column.set_visible(True) + #contactsview.append_column(column) + + self._contactsviewselection = contactsview.get_selection() + self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE) + + return False + + def populate_callback_combo(self): + self._callbackList = gtk.ListStore(gobject.TYPE_STRING) + for number, description in self._gcBackend.get_callback_numbers().iteritems(): + self._callbackList.append((make_pretty(number),)) + + combobox = self._widgetTree.get_widget("callbackcombo") + combobox.set_model(self._callbackList) + combobox.set_text_column(0) + + combobox.get_child().set_text(make_pretty(self._gcBackend.get_callback_number())) + + def _idly_populate_recentview(self): + self._recentmodel.clear() + + for personsName, phoneNumber, date, action in self._gcBackend.get_recent(): + description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber) + item = (phoneNumber, description) + gtk.gdk.threads_enter() + self._recentmodel.append(item) + gtk.gdk.threads_leave() + + self._recenttime = time.time() + return False + + @make_idler + def _idly_populate_contactsview(self): + self._contactsmodel.clear() + + # completely disable updating the treeview while we populate the data + contactsview = self._widgetTree.get_widget("contactsview") + contactsview.freeze_child_notify() + contactsview.set_model(None) + + contactType = (self._addressBook.factory_short_name(),) + for contactId, contactName in self._addressBook.get_contacts(): + self._contactsmodel.append(contactType + (contactName, "", contactId) + ("",)) + yield + + # restart the treeview data rendering + contactsview.set_model(self._contactsmodel) + contactsview.thaw_child_notify() + + self._contactstime = time.time() + + def attempt_login(self, numOfAttempts = 1): + """ + @note Not meant to be called directly, but run as a seperate thread. + """ + assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts" + + if not self._deviceIsOnline: + warnings.warn("Attempted to login while device was offline", UserWarning, 2) + return False + + if self._gcBackend.is_authed(): + return True + + for x in xrange(numOfAttempts): + gtk.gdk.threads_enter() + + dialog = self._widgetTree.get_widget("login_dialog") + dialog.set_transient_for(self._window) + dialog.set_default_response(0) + dialog.run() + + username = self._widgetTree.get_widget("usernameentry").get_text() + password = self._widgetTree.get_widget("passwordentry").get_text() + self._widgetTree.get_widget("passwordentry").set_text("") + dialog.hide() + gtk.gdk.threads_leave() + loggedIn = self._gcBackend.login(username, password) + if loggedIn: + gtk.gdk.threads_enter() + if self._gcBackend.get_callback_number() is None: + self._gcBackend.set_sane_callback() + self.populate_callback_combo() + self.set_account_number(self._gcBackend.get_account_number()) + gtk.gdk.threads_leave() + return True + + def display_error_message(self, msg): + error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg) + + def close(dialog, response, editor): + editor.about_dialog = None + dialog.destroy() + error_dialog.connect("response", close, self) + error_dialog.run() + + 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 (i, bookId), (factory.factory_name(), bookName) + + def open_addressbook(self, bookFactoryId, bookId): + self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId) + self._contactstime = 0 + gobject.idle_add(self._idly_populate_contactsview) + + def set_number(self, number): + """ + Set the callback phonenumber + """ + self._phonenumber = make_ugly(number) + self._prettynumber = make_pretty(self._phonenumber) + self._numberdisplay.set_label("%s" % (self._prettynumber)) + + def set_account_number(self, number): + """ + Displays current account number + """ + self._widgetTree.get_widget("gcnumber_display").set_label("%s" % (number)) + + @staticmethod + def _on_close(*args, **kwds): + 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 + """ + if memory_low: + self._gcBackend.clear_caches() + gc.collect() + + def _on_connection_change(self, connection, event, magicIdentifier): + """ + @note Hildon specific + """ + 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: + self._window.set_sensitive(True) + self._deviceIsOnline = True + threading.Thread(target=self.attempt_login,args=[2]).start() + elif status == conic.STATUS_DISCONNECTED: + self._window.set_sensitive(False) + self._deviceIsOnline = False + + def _on_window_state_change(self, widget, event, *args): + """ + @note Hildon specific + """ + if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: + self._isFullScreen = True + else: + self._isFullScreen = False + + def _on_key_press(self, widget, event, *args): + """ + @note Hildon specific + """ + if event.keyval == gtk.keysyms.F6: + if self._isFullScreen: + self._window.unfullscreen() + else: + self._window.fullscreen() + + def _on_loginbutton_clicked(self, *args): + self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK) + + def _on_loginclose_clicked(self, *args): + self._on_close() + sys.exit(0) + + def _on_clearcookies_clicked(self, *args): + self._gcBackend.logout() + self._recenttime = 0.0 + self._contactstime = 0.0 + self._recentmodel.clear() + self._widgetTree.get_widget("callbackcombo").get_child().set_text("") + self.set_account_number("") + + # re-run the inital grandcentral setup + threading.Thread(target=self.attempt_login,args=[2]).start() + #gobject.idle_add(self._idly_populate_callback_combo) + + def _on_callbackentry_changed(self, *args): + """ + @todo Potential blocking on web access, maybe we should defer this or put up a dialog? + """ + text = make_ugly(self._widgetTree.get_widget("callbackcombo").get_child().get_text()) + if not self._gcBackend.is_valid_syntax(text): + warnings.warn("%s is not a valid callback number" % text, UserWarning, 2) + elif text == self._gcBackend.get_callback_number(): + warnings.warn("Callback number already is %s" % self._gcBackend.get_callback_number(), UserWarning, 2) + else: + self._gcBackend.set_callback_number(text) + + def _on_recentview_row_activated(self, treeview, path, view_column): + model, itr = self._recentviewselection.get_selected() + if not itr: + return + + self.set_number(self._recentmodel.get_value(itr, 0)) + self._notebook.set_current_page(0) + self._recentviewselection.unselect_all() + + def _on_addressbook_combo_changed(self, *args, **kwds): + combobox = self._widgetTree.get_widget("addressbook_combo") + itr = combobox.get_active_iter() + + factoryId = int(self._booksList.get_value(itr, 0)) + bookId = self._booksList.get_value(itr, 1) + self.open_addressbook(factoryId, bookId) + + def _on_contactsview_row_activated(self, treeview, path, view_column): + model, itr = self._contactsviewselection.get_selected() + if not itr: + return + + contactId = self._contactsmodel.get_value(itr, 3) + contactDetails = self._addressBook.get_contact_details(contactId) + contactDetails = [phoneNumber for phoneNumber in contactDetails] + + if len(contactDetails) == 0: + phoneNumber = "" + elif len(contactDetails) == 1: + phoneNumber = contactDetails[0][1] + else: + phoneNumber = self._phoneTypeSelector.run(contactDetails) + + if 0 < len(phoneNumber): + self.set_number(phoneNumber) + self._notebook.set_current_page(0) + + self._contactsviewselection.unselect_all() + + def _on_notebook_switch_page(self, notebook, page, page_num): + if page_num == 1 and 300 < (time.time() - self._contactstime): + threading.Thread(target=self._idly_populate_contactsview).start() + elif page_num == 2 and 300 < (time.time() - self._recenttime): + threading.Thread(target=self._idly_populate_recentview).start() + #elif page_num == 3 and self._callbackNeedsSetup: + # gobject.idle_add(self._idly_populate_callback_combo) + + tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text() + if hildon is not None: + self._window.set_title(tabTitle) + else: + self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle)) + + def _on_dial_clicked(self, widget): + """ + @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog? + """ + loggedIn = self._gcBackend.is_authed() + if not loggedIn: + return + #loggedIn = self.attempt_login(2) + + if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "": + self.display_error_message("Backend link with grandcentral is not working, please try again") + warnings.warn("Backend Status: Logged in? %s, Authenticated? %s, Callback=%s" % (loggedIn, self._gcBackend.is_authed(), self._gcBackend.get_callback_number()), UserWarning, 2) + return + + try: + callSuccess = self._gcBackend.dial(self._phonenumber) + except ValueError, e: + self._gcBackend._msg = e.message + callSuccess = False + + if not callSuccess: + self.display_error_message(self._gcBackend._msg) + else: + self.set_number("") + + self._recentmodel.clear() + self._recenttime = 0.0 + + def _on_paste(self, *args): + contents = self._clipboard.wait_for_text() + self.set_number(phoneNumber) + + def _on_clear_number(self, *args): + self.set_number("") + + def _on_digit_clicked(self, widget): + self.set_number(self._phonenumber + widget.get_name()[5]) + + def _on_backspace(self, widget): + self.set_number(self._phonenumber[:-1]) + + def _on_clearall(self): + self.set_number("") + return False + + def _on_back_pressed(self, widget): + self._clearall_id = gobject.timeout_add(1000, self._on_clearall) + + def _on_back_released(self, widget): + if self._clearall_id is not None: + gobject.source_remove(self._clearall_id) + self._clearall_id = None + + def _on_about_activate(self, *args): + dlg = gtk.AboutDialog() + dlg.set_name(self.__pretty_app_name__) + dlg.set_version(self.__version__) + dlg.set_copyright("Copyright 2008 - LGPL") + dlg.set_comments("Dialer is designed to interface with your Google Grandcentral account. This application is not affiliated with Google or Grandcentral in any way") + dlg.set_website("http://gc-dialer.garage.maemo.org/") + dlg.set_authors(["","Eric Warnke ","Ed Page "]) + dlg.run() + dlg.destroy() + + +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() + if hildon is not None: + gtk.set_application_name(Dialpad.__pretty_app_name__) + title = 'Dialpad' + handle = Dialpad() + gtk.main() + + +class DummyOptions(object): + + def __init__(self): + self.test = False + + +if __name__ == "__main__": + 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() diff --git a/src/icons/hicolor/26x26/hildon/dialcentral.png b/src/icons/hicolor/26x26/hildon/dialcentral.png new file mode 100644 index 0000000000000000000000000000000000000000..df50c66807a4ac5a11657771901db51b56e2d2c6 GIT binary patch literal 1671 zcmV;226*|2P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2iOK2 z4=f7;iQIkw00sw1L_t(Y$EB5hOk39(#(($v+P*dcyI^C8Nj%6$p;`i>sX$$pc7bjJ zG?0L-iNX?ziH=B1rfHfckt{@!l0UXcQ=(`}qNtsS@}aV}sEUXpmu|2qDM^{MFJ(<# zzy=x^5DZ+dudlD~-5)Df)}~9RJ^!2|o%25LqxXHzdjvki<;$0?{{DVdlB7TT{r=v~ zn>R0On&wzNR}=-)G|_b(!!R&S6Gc(bb)7^afv)Qaf>0a?1YSLT`t*7L0bp)!?s#5a z-Y-^(PM$nTS63HLgQ6%zA`v2y2on<%BoYZKDk^AbXb>{wmStIw2X%FIQBqPuZ*MQ@ zbQ(bru-okjf`H9t%d9q=4M~!?diCmmBt3roxZd5}?UbIRXlZE)S(d4*s{_aot?nHT z2a+UVv)Q=)!3WI6<2?H3BD$_qy1tmBM~^zjJ{hZeB6%W_VB^M(w6(Q8wKQ(OpS0IY zfy0Yzfb?0Ho-KH`K?2Zq9f!k#VHoJTPBNLKy1JUFsVQb>XOSccQ4|qH5lz!F&2?sF z;r9E9#Z6u)ucEjMewBNIx4duQ`PdT?0mjG2Gv&}Ujg^%Z0D{3FB_$;k6%}EcCfBZA zBQGxxpU;QO<-%n*S-&=mg4Uf78(}>8A-m?k!}FyTq>Pj+qZ}ZaOrokPsZDcGQ8IhxXHL;3SN_66Qxy48HT~~@-l{DuxcJr z6p4ESXfvfnM|T-8iF7&FfhP{3m2H4o@Q`xkexesQdn3>b8|C0cI@Eb!5>jt zD6#Jc2Pu8|+idt+A?42nxO?UX+iSkX>zChNCzY3%+m@D=SYBR6QIyQ1si~>q)~#EZ zrU^hG5TL24iTn5OqiGs#-`|Z#8|T)9os(}%WO+?m>)~fxHj|a@!Z3`aB#L6tvMf|p zC7n)Z5&%RZ5ze1K55Tcw$B0IwbaZt5d#=pK2*26SyHPK5??jms99(+PK;5nfeEPsd z)3mjs)9JLSs+v)rOeWEFo&NrQ_Uze1XJ;p>s&eAQ2?`1dICA6&_wL=})0s!SQu`t$ z*^dx%mXZ8An{vm=T9b{IR8bVgA^Cj1(CFwWs;aWGvO+SM#4rr%>+2aA8DaPC-2m+0 zzaO{TO?P)U#Vw88&R#Kwn=UilT7p)G0Jg2!LqtgMV^G>WEa#N%;NsT79}9pcKBE9B(lke{DVRaF%;GczA^B|8XM}7<$T;OH#r^;Nm1Zx?qto{6GjmP77yFate%>h`bjhz-FoNFoxV5HlA@AtupscLyE8qV@SuD%?HSi4ZF)$Cb0pIwN z_@4qM;0MZq9}9x;$(Lrco`A*0MQd?!(Tc@l)>H7t<6K|(ps!!Qe)sIzv)))N_QzBz z^>Ev^ZKV!}qg9q=(&;p^ER#y5n4h1Igu~&!O`A4h7)B@*3bnSkx1V~P>t9a#+JnYl RY6buR002ovPDHLkV1gVhDY*au literal 0 HcmV?d00001 diff --git a/src/icons/hicolor/64x64/hildon/dialcentral.png b/src/icons/hicolor/64x64/hildon/dialcentral.png new file mode 100644 index 0000000000000000000000000000000000000000..8d98390d0478e4bbef85641af67982cdd29ab00c GIT binary patch literal 6411 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 LUFGt}R$>1G8dR*- literal 0 HcmV?d00001 diff --git a/src/icons/hicolor/scalable/hildon/dialcentral.png b/src/icons/hicolor/scalable/hildon/dialcentral.png new file mode 100644 index 0000000000000000000000000000000000000000..a8753506901fcff1ea8d827774b21ac938bf3026 GIT binary patch literal 32182 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 OilMHV&O>ddsQ&|#ca-%2 literal 0 HcmV?d00001 diff --git a/support/DEBIAN/control b/support/DEBIAN/control new file mode 100644 index 0000000..a2b3084 --- /dev/null +++ b/support/DEBIAN/control @@ -0,0 +1,40 @@ +Package: dialcentral +Version: 0.8.0-9 +Section: user/communication +Priority: extra +Architecture: all +Depends: python2.5, python2.5-gtk2 +Installed-Size: 244 +Maintainer: Eric Warnke +Description: Simple interface to Google's GrandCentral(tm) service +Maemo-Icon-26: + iVBORw0KGgoAAAANSUhEUgAAABcAAAAaCAYAAABctMd+AAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A + /wD/oL2nkwAAAAlwSFlzAAAN1wAADdcBQiibeAAAAAd0SU1FB9gGHA8sCwKJ3H4AAAYHSURBVEjH + pZV9TFvXGcZ/9/ravjYBu2BjiEk8yEehWgKiqUBdlnaBbgI0kICsicISiYmOSEumaZomkSxEkZI/ + tkhTomhKoqidiPKhsraoiogil26wKClJmbQvZU1dwAYaGBAMXK6vr6/v3R8rVtamS6c9/5wjnfM+ + 76P3fc57BD7D5cuXrf7+flWSpD/7/f7ezZs3L2ua5lg9VxQF0zTRdR3DMDBNE0VR0HWdRCKBrusI + glAcCARePX369HoAAWBubu54Xl7eL1aJTp48SVdXF0+DoihEIhEikQgTExMkEgkqKipoaGgQMuSW + ZVmPB3V1dVFSUkJvby/pdBpBELDb7QiCgM1my6w2mw1Jkrh69ep/JD1+/Lje3d3tlJ6kaGlpCVmW + qaurA8gQre4dDgeSJGGz2bj9wQfMxeM8+uciuq5Tur6Yo0ePOsY+Gat6InkikWDjxo20tbU9tTRu + v5+010uBw4tsgPRZl54teLZBAtB1HYfDgWEY6LpOMpmkurqaqakpZmdnkSQJURQRRRFN0zLNdWZl + 4fb7icVNXiivorgLfpV7gre8b+D52BMRAcbHxzPl0DSNlZUVAILBICUlJRQVFWGaJteuXSMvLw+f + z4fL5cJlN1m/NouC1p0QG2E8+SG75r/D86UVpIyUK6McIJlMoqoqqVQKAFVVWVhYQJIkwuEwra2t + XLx4kZaWFiQJ/NkihfkyvNcPuMm4Ygpek18TpVVLGYbB8vIyhmGwah5RFJEkCafTSVVVFQCVlZUA + 2AWTQo+dgqrzzNyNAUXI5S28/r3fsST4eKg8DIqrihVFQVVVEolEplmyLBMMBsnNzWXr1q04HA6q + q6txuVx4PB7wOtj1Ug72QA7ZGx18d8c00ppl3H4HpmAaUmdn5zpFUVhYWCCZTKJpGoIgMDAwwIUL + F5ienmZwcJCdO3dSWFhIc3MzO3bs4ODBH1FaKLHvBwcpffnbbPhaIeXPBbh75gbba77F65ffXieV + l5fblpaWWF5eRlGUzKOpqanh1q1bmKYJQCAQoKmpifv376NpGm3f341HG+fWhJ2Tb0tkeU1a6+Fn + WzaTle3CMIykJIpi0LIsVFUlnU5nEgBEIhHOnz8PwLFjx4hGo3R0dPx7rszHCL/Zz7tRL3PvRJkT + HFx60EDdrgd8+sBE07S1otPptKmqmlGeTCbRdZ3+/n727NlDZ2cnqqpy4sQJCgoKOHLkCPfu3ePT + mUe8UvsiJdmPEHKWkfw6m3LHyVqTjZZUURTFIfl8vtDo6CiqqrKyskIymcQwDOrr6xkZGWH37t0A + 7N+/H7fbTXd3N+Pj4wTy1hB0ynyz5hFmaBtF64r4eqmPv/1hiBdeqcX6Tc8zoiAIvry8vIy30+k0 + pmly5swZNmzYQF9fH4qicOrUKTRN49y5c4iiyF/++nc+fDDF7z8SeW+8krc+ep7BqRB1jbuoqHoJ + TdPWSul0erCsrIxoNIqmacTjcVKpFIcOHeLKlSvk5OSQn59PVVUVMzMzNDc3MzY2ht3uxu6S6aic + 54fPXYJ0mqTuJjKxlvdHfovT6UxJjY2Nf7Isi3A4zPDwMJOTkxQXFzM0NMSNGzeQZZloNEo0GmV0 + dJR9+/bh8/no6enBMnQaG+qR5XzcLjc5Hg9JUeBq7yX27t37sgQgCILw+Zkej8dpaWlhaGiIUChE + UVERbW1t3Lx5k2AwyOHDh5mcnCQrK4tAIMDo6Ch9N99nYmKCAwcOvNrY2DgpPE5oWVYBMAKs/fxo + jcfjeL1exsbG8Hg85ObmMjAwwO3bt5mdnQWgtrb2l01NTT9fjRH+26yempr6STQa3Xrnzp3C6enp + F4eHh7NdLhfbt2/vCYVCXpfL9Y/S0tL+LVu2DPL/or293WpqarK+6n3xfyHv6OigrKzsK9//Qlks + y/o18AzwMTAPtAHf+JL4nwIm4AfKgR8LgvDJl2aznoDFxUVrcXHRisVi1lPwxuNcX/igr1+/fvfs + 2bPeWCz2x1Qq9XDbtm2lDoejVZZl0uk0siyTSqWYn5+PhMPhvk2bNmEYRigUCrW2t7efepzrX076 + 2oPGX2oGAAAAAElFTkSuQmCC diff --git a/support/DEBIAN/postinst b/support/DEBIAN/postinst new file mode 100755 index 0000000..d81499b --- /dev/null +++ b/support/DEBIAN/postinst @@ -0,0 +1,3 @@ +#!/bin/sh + +gtk-update-icon-cache /usr/share/icons/hicolor -- 1.7.9.5