#!/usr/bin/python
+from __future__ import with_statement
+
import logging
+try:
+ import cPickle
+ pickle = cPickle
+except ImportError:
+ import pickle
+
+import constants
import util.coroutines as coroutines
+import util.misc as misc_utils
+import util.go_utils as gobject_utils
-_moduleLogger = logging.getLogger("gvoice.addressbook")
+_moduleLogger = logging.getLogger(__name__)
class Addressbook(object):
- def __init__(self, backend):
+ _RESPONSE_GOOD = 0
+ _RESPONSE_BLOCKED = 3
+
+ OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("0.8.0")
+
+ def __init__(self, backend, asyncPool):
self._backend = backend
- self._contacts = {}
- self._addedContacts = set()
- self._removedContacts = set()
- self._changedContacts = set()
+ self._numbers = {}
+ self._asyncPool = asyncPool
self.updateSignalHandler = coroutines.CoTee()
- def update(self, force=False):
- if not force and self._contacts:
+ def load(self, path):
+ _moduleLogger.debug("Loading cache")
+ assert not self._numbers
+ try:
+ with open(path, "rb") as f:
+ fileVersion, fileBuild, contacts = pickle.load(f)
+ except (pickle.PickleError, IOError, EOFError, ValueError, Exception):
+ _moduleLogger.exception("While loading")
return
- oldContacts = self._contacts
- oldContactIds = set(self.get_contacts())
-
- self._contacts = {}
- self._populate_contacts()
- newContactIds = set(self.get_contacts())
-
- self._addedContacts = newContactIds - oldContactIds
- self._removedContacts = oldContactIds - newContactIds
- self._changedContacts = set(
- contactId
- for contactId in newContactIds.intersection(oldContactIds)
- if self._has_contact_changed(contactId, oldContacts)
- )
-
- message = self, self._addedContacts, self._removedContacts, self._changedContacts
- self.updateSignalHandler.send(message)
-
- def get_contacts(self):
- return self._contacts.iterkeys()
- def get_contact_name(self, contactId):
- return self._contacts[contactId][0]
+ if contacts and misc_utils.compare_versions(
+ self.OLDEST_COMPATIBLE_FORMAT_VERSION,
+ misc_utils.parse_version(fileVersion),
+ ) <= 0:
+ _moduleLogger.info("Loaded cache")
+ self._numbers = contacts
+ self._loadedFromCache = True
+ else:
+ _moduleLogger.debug(
+ "Skipping cache due to version mismatch (%s-%s)" % (
+ fileVersion, fileBuild
+ )
+ )
+
+ def save(self, path):
+ _moduleLogger.info("Saving cache")
+ if not self._numbers:
+ _moduleLogger.info("Odd, no conversations to cache. Did we never load the cache?")
+ return
- def get_contact_details(self, contactId):
- self._populate_contact_details(contactId)
- return self._get_contact_details(contactId)
+ try:
+ dataToDump = (constants.__version__, constants.__build__, self._numbers)
+ with open(path, "wb") as f:
+ pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
+ except (pickle.PickleError, IOError):
+ _moduleLogger.exception("While saving for %s" % self._name)
+ _moduleLogger.info("Cache saved")
- def _populate_contacts(self):
- if self._contacts:
+ def update(self, force=False):
+ if not force and self._numbers:
return
- contacts = self._backend.get_contacts()
- for contactId, contactName in contacts:
- self._contacts[contactId] = (contactName, {})
- def _populate_contact_details(self, contactId):
- if self._get_contact_details(contactId):
+ le = gobject_utils.AsyncLinearExecution(self._asyncPool, self._update)
+ le.start()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _update(self):
+ try:
+ contacts = yield (
+ self._backend.get_contacts,
+ (),
+ {},
+ )
+ except Exception:
+ _moduleLogger.exception("While updating the addressbook")
return
- self._get_contact_details(contactId).update(
- self._backend.get_contact_details(contactId)
- )
- def _get_contact_details(self, contactId):
- return self._contacts[contactId][1]
+ oldContacts = self._numbers
+ oldContactNumbers = set(self.get_numbers())
+
+ self._numbers = self._populate_contacts(contacts)
+ newContactNumbers = set(self.get_numbers())
- def _has_contact_changed(self, contactId, oldContacts):
- oldContact = oldContacts[contactId]
- oldContactName = oldContact[0]
- oldContactDetails = oldContact[1]
- if oldContactName != self.get_contact_name(contactId):
- return True
- if not oldContactDetails[1]:
+ addedContacts = newContactNumbers - oldContactNumbers
+ removedContacts = oldContactNumbers - newContactNumbers
+ changedContacts = set(
+ contactNumber
+ for contactNumber in newContactNumbers.intersection(oldContactNumbers)
+ if self._numbers[contactNumber] != oldContacts[contactNumber]
+ )
+
+ if addedContacts or removedContacts or changedContacts:
+ message = self, addedContacts, removedContacts, changedContacts
+ self.updateSignalHandler.stage.send(message)
+
+ def get_numbers(self):
+ return self._numbers.iterkeys()
+
+ def get_contact_name(self, strippedNumber):
+ """
+ @throws KeyError if contact not in list (so client can choose what to display)
+ """
+ return self._numbers[strippedNumber][0]
+
+ def get_phone_type(self, strippedNumber):
+ try:
+ return self._numbers[strippedNumber][1]
+ except KeyError:
+ return "unknown"
+
+ def is_blocked(self, strippedNumber):
+ try:
+ return self._numbers[strippedNumber][2]["response"] == self._RESPONSE_BLOCKED
+ except KeyError:
return False
- # if its already in the old cache, purposefully add it into the new cache
- return oldContactDetails != self.get_contact_details(contactId)
+
+ def _populate_contacts(self, contacts):
+ numbers = {}
+ for contactId, contactDetails in contacts:
+ contactName = contactDetails["name"]
+ contactNumbers = (
+ (
+ misc_utils.normalize_number(numberDetails["phoneNumber"]),
+ numberDetails.get("phoneType", "Mobile"),
+ )
+ for numberDetails in contactDetails["numbers"]
+ )
+ numbers.update(
+ (number, (contactName, phoneType, contactDetails))
+ for (number, phoneType) in contactNumbers
+ )
+ return numbers
+
+
+def print_addressbook(path):
+ import pprint
+
+ try:
+ with open(path, "rb") as f:
+ fileVersion, fileBuild, contacts = pickle.load(f)
+ except (pickle.PickleError, IOError, EOFError, ValueError):
+ _moduleLogger.exception("")
+ else:
+ pprint.pprint((fileVersion, fileBuild))
+ pprint.pprint(contacts)