package pl.szpadel.android.gadu;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
import pl.szpadel.android.gadu.packets.AddNotify;
import pl.szpadel.android.gadu.packets.BasePacket;
import pl.szpadel.android.gadu.packets.Notify;
import pl.szpadel.android.gadu.packets.NotifyReply80;
import pl.szpadel.android.gadu.packets.ReceivedPacket;
import pl.szpadel.android.gadu.packets.UserlistReply80;
import pl.szpadel.android.gadu.packets.UserlistRequest;
import pl.szpadel.android.gadu.packets.ZeroLenghtSendPacket;
import android.content.Context;
import android.database.DataSetObserver;
/** Singleton containing all contact data */
public class ContactInfoManager extends Object implements IAppComponent {
private static final String TAG = "ContactInfoManager";
///////////////////////
// Contacts db
/// Actual database
private HashMap<Long, ContactInfo> mById = new HashMap<Long, ContactInfo>();
/// UIn of logged in user for which contacts are showed
private long mCurrentUin = 0;
/// buffer from incoming userlist messages
private ArrayList<ByteBuffer> mReceivedBuffers = new ArrayList<ByteBuffer>();
/// Contacts to which we subscribed in the server
private HashSet<Long> mSubscribedContacts = new HashSet<Long>();
/// Flag - contact were loaded from DB during this session
private boolean mContactsLoaded = false;
ContactDatabaseOpenHelper mDBHelper;
/// Adds user to the info manager, or updates if already exists
public synchronized void addContact(ContactInfo contact) {
if (mById.containsKey(contact.id)) {
updateInDB(contact);
} else {
addToDB(contact);
}
mById.put(contact.id, contact);
// if connected and not subscribed to contact state, do it.
if (App.getInstance().getConnectionState().getState() == ConnectionState.CONNECTED) {
if (! mSubscribedContacts.contains(contact.id)) {
subscribeToSingleContact(contact.id);
}
}
notifyObservers();
}
/// Returns contact by uin
public synchronized ContactInfo getByUin(long uin) {
return mById.get(uin);
}
/// Request download from server
public void importContactListFromServer() {
AGLog.i(TAG, "importContactListFromServer");
// create req message
App.getInstance().getCommunicationService().sendPacket(new UserlistRequest());
// prepare to receive packets
mReceivedBuffers.clear();
}
/// Handles incoming userlist packet
private void handleUserlistPacket(UserlistReply80 packet) {
AGLog.i(TAG, "onUserlistPacket");
mReceivedBuffers.add(packet.data);
if (packet.last) {
// count total data
int compressedDataLen = 0;
for (ByteBuffer b: mReceivedBuffers) {
compressedDataLen += b.limit();
}
AGLog.i(TAG, "total compressed data size:" + compressedDataLen);
// allocate buffer to hold them all
ByteBuffer compressedData = ByteBuffer.allocate(compressedDataLen);
for (ByteBuffer b: mReceivedBuffers) {
b.rewind();
compressedData.put(b);
}
// allocate data for decompressed
byte[] decompressedData = new byte[compressedDataLen*100]; // i don't like this
// decompress
Inflater inflater = new Inflater();
inflater.setInput(compressedData.array());
try {
int decompressedDataLen = inflater.inflate(decompressedData, 0, decompressedData.length);
AGLog.i(TAG, "decompressed data size:"+ decompressedDataLen);
String asString = new String(decompressedData, 0, decompressedDataLen);
//AGLog.i(TAG, "Contact list:\n" + asString);
parseServerContactList(asString);
} catch (DataFormatException e) {
AGLog.e(TAG, "Error decompressing received list:"+ e.getMessage());
}
}
}
/// Updates
/// Returns shallow copy all contacts
public synchronized Collection<ContactInfo> getContacts() {
return mById.values();
}
/// Private constructor
public ContactInfoManager(Context ctx) {
mDBHelper = new ContactDatabaseOpenHelper(ctx);
}
/////////////////////////////////////
// Change notifications
/// Set of observes. We need no more than one instance of every observer
private HashSet<DataSetObserver> mObservers = new HashSet<DataSetObserver>();
/// Flag - bulk change in progress
boolean mBulkChange = false;
/// changes during bulk change operation
int mBulkChangeChanges = 0;
////////////////////////////////////////////////////////
// interface
/// starts bulk change - all changes will be applied only after endChanges() is called
public void beginChanges() {
assert(mBulkChange == false);
mBulkChange = true;
mBulkChangeChanges = 0;
}
/// Ends bulk change. Notifies observes if something happened since last beginChanges()
public void endChanges() {
assert(mBulkChange == true);
mBulkChange = false;
if (mBulkChangeChanges > 0 ) {
for (DataSetObserver observer : mObservers ) {
observer.onChanged();
}
mBulkChangeChanges = 0;
}
}
public void registerDataSetObserver(DataSetObserver observer) {
mObservers.add(observer);
}
public void unregisterDataSetObserver(DataSetObserver observer) {
mObservers.remove(observer);
}
// Notifies observers of change
private void notifyObservers() {
if (mBulkChange == true) {
AGLog.d(TAG, "notifyObservers: doing nothing, bulk change");
mBulkChangeChanges++;
} else {
AGLog.d(TAG, "notifyObservers: notyfying");
for (DataSetObserver observer : mObservers ) {
observer.onChanged();
}
}
}
@Override
public void onPacket(ReceivedPacket packet) {
switch(packet.getType()) {
case BasePacket.TYPE_NOTIFY_REPLY80:
case BasePacket.TYPE_STATUS80:
updateStatuses((NotifyReply80) packet);
break;
case BasePacket.TYPE_USERLIST_REPLY80:
handleUserlistPacket((UserlistReply80)packet);
break;
default:
; // do nothing
}
}
@Override
public void onConnectionStateChanged(ConnectionState state) {
// just connected?
if (state.getState() == ConnectionState.CONNECTED) {
// logged in user changed?
if (mCurrentUin != App.getInstance().getConfig().getUin()) {
// clear current user list
mById.clear();
mContactsLoaded = false;
mCurrentUin = App.getInstance().getConfig().getUin();
notifyObservers();
}
// load contacts from DB if not yet loaded
if (!mContactsLoaded) {
loadContactsFromDB();
mContactsLoaded = true;
}
// subscribe to contacts
mSubscribedContacts.clear();
subscribeToAllContacts();
}
// disconnected?
else {
// set all statuses to 'offline'
AGLog.i(TAG, "Connection lost, setting al statuses to offline");
setAllContactsOffline();
}
}
// Helper - translates uin to name
public String uinToName(long uin) {
if (mById.containsKey(uin)){
ContactInfo ci = mById.get(uin);
if (ci.displayName != "") {
return ci.displayName;
}
}
return Long.toString(uin);
}
/// Loads contact from DB
public void loadContacts() {
if (!mContactsLoaded) {
loadContactsFromDB();
mContactsLoaded = true;
}
}
/// deletes contact
public void deleteContact(long uin) {
if (mById.containsKey(uin)){
mById.remove(uin);
mDBHelper.deleteContact(uin);
notifyObservers();
}
}
/////////////////////////////////////////////////////////
// Implementation
private void subscribeToAllContacts() {
// subscribes to all contacts
// send contact list
AGLog.i(TAG, "Subscrining to the entire contact list");
if (mById.isEmpty()){
AGLog.d(TAG, "Sending empty contact list");
ZeroLenghtSendPacket cl = new ZeroLenghtSendPacket(BasePacket.TYPE_LIST_EMPTY);
App.getInstance().getCommunicationService().sendPacket(cl);
}
else {
// TODO handle contact lists above 400
assert(mById.size() <= 400);
AGLog.d(TAG, "Sending list notification");
Notify notify = new Notify(mById.values(), true);
App.getInstance().getCommunicationService().sendPacket(notify);
mSubscribedContacts.addAll(mById.keySet());
}
}
private void subscribeToSingleContact(long id) {
AGLog.i(TAG, "Subscribint to single packet: " + id);
AddNotify packet = new AddNotify((int)id);
App.getInstance().getCommunicationService().sendPacket(packet);
}
/// contact list parser handler
private class ContactListHandler extends DefaultHandler {
/// Last element content
private String mElementContent = new String();
ContactInfo mContact = null; // currently parsed contact
/// On element started
public void startElement (String uri, String localName, String qName, Attributes attributes) {
if (localName == "Contact") {
mContact = new ContactInfo("", -1);
}
}
/// On element content
public void characters (char[] ch, int start, int length) {
mElementContent = new String(ch, start, length);
}
/// On element ended
public void endElement (String uri, String localName, String qName) {
if (mContact != null) {
try {
AGLog.d(TAG, "parsing contact param: "+ localName + " = " + mElementContent);
if (localName == "GGNumber") mContact.id = Long.parseLong(mElementContent);
else if (localName == "ShowName" )mContact.displayName = mElementContent;
else if (localName == "MobilePhone" )mContact.mobilePhone = mElementContent;
else if (localName == "HomePhone" )mContact.homePhone = mElementContent;
else if (localName == "Email" )mContact.email = mElementContent;
else if (localName == "FirstName" )mContact.firstName = mElementContent;
else if (localName == "LastName" )mContact.lastName = mElementContent;
else if (localName == "WwwAddress" )mContact.wwwAdrress = mElementContent;
else if (localName == "Gender" )mContact.gender = mElementContent;
else if (localName == "Birth" )mContact.birth = mElementContent;
else if (localName == "City" )mContact.city = mElementContent;
else if (localName == "Contact") {
// validate
if (mContact.id < 0){
AGLog.e(TAG, "got from XML conatct with no GGNumber: " + mContact);
} else {
addContact(mContact);
}
mContact = null;
}
} catch (Exception e) {
AGLog.e(TAG, "Error parsing contact: " + mContact);
mContact = null; // will skip it and wait for the next one
}
}
}
}
/// Parses contact list as send by a server
// format description: http://dev.gg.pl/api/pages/formaty_plikow.html
private void parseServerContactList(String xml) {
AGLog.i(TAG, "parsing xml");
try {
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
InputSource is = new InputSource(new StringReader(xml));
beginChanges();
parser.parse(is, new ContactListHandler());
endChanges();
// set user list to top
if (!mById.isEmpty()) {
ContactListActivity activity = App.getInstance().getContactListActivity();
if (activity != null) {
activity.setSelection(0);
}
}
} catch (Exception e) {
AGLog.e(TAG, "Error parsing contact list:" + e.getMessage());
}
}
/// Updates statuses
private void updateStatuses(NotifyReply80 statuses) {
for (NotifyReply80.StatusInfo status : statuses.statuses){
AGLog.d(TAG, "Updating status: " + status);
ContactInfo ci = mById.get(status.uin);
if (ci != null) {
AGLog.i(TAG, "Updating status for user " + status.uin + " to " + status.status);
ci.status = status.status;
}
}
notifyObservers();
}
/// Sets all contact statuses to offline
private void setAllContactsOffline() {
// change all statuses
for(ContactInfo ci : mById.values()) {
ci.status = new Status(Status.GG_STATUS_NOT_AVAIL, ci.status.getDescription());
}
// tell everyone
notifyObservers();
}
@Override
public void onStatusChanged(Status status) {
// nothing
}
@Override
public void onConfigChanged() {
// TODO Auto-generated method stub
}
/// Adds single contacts to DB
private void addToDB(ContactInfo contact) {
mDBHelper.insert(contact);
}
/// updates single contact in db
private void updateInDB(ContactInfo contact) {
mDBHelper.update(contact);
}
/// load all contacts from DB
private void loadContactsFromDB() {
Collection<ContactInfo> contacts = mDBHelper.getAllContacts();
if (!contacts.isEmpty()) {
for (ContactInfo info : contacts) {
mById.put(info.id, info);
}
notifyObservers();
}
}
}
|