XmppConnectionService.java

   1package eu.siacs.conversations.services;
   2
   3import java.text.ParseException;
   4import java.text.SimpleDateFormat;
   5import java.util.Date;
   6import java.util.Hashtable;
   7import java.util.List;
   8import java.util.Random;
   9
  10import org.openintents.openpgp.util.OpenPgpApi;
  11import org.openintents.openpgp.util.OpenPgpServiceConnection;
  12
  13import net.java.otr4j.OtrException;
  14import net.java.otr4j.session.Session;
  15import net.java.otr4j.session.SessionStatus;
  16
  17import eu.siacs.conversations.crypto.PgpEngine;
  18import eu.siacs.conversations.crypto.PgpEngine.OpenPgpException;
  19import eu.siacs.conversations.entities.Account;
  20import eu.siacs.conversations.entities.Contact;
  21import eu.siacs.conversations.entities.Conversation;
  22import eu.siacs.conversations.entities.Message;
  23import eu.siacs.conversations.entities.MucOptions;
  24import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
  25import eu.siacs.conversations.entities.Presences;
  26import eu.siacs.conversations.persistance.DatabaseBackend;
  27import eu.siacs.conversations.persistance.OnPhoneContactsMerged;
  28import eu.siacs.conversations.ui.OnAccountListChangedListener;
  29import eu.siacs.conversations.ui.OnConversationListChangedListener;
  30import eu.siacs.conversations.ui.OnRosterFetchedListener;
  31import eu.siacs.conversations.utils.ExceptionHelper;
  32import eu.siacs.conversations.utils.MessageParser;
  33import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
  34import eu.siacs.conversations.utils.PhoneHelper;
  35import eu.siacs.conversations.utils.UIHelper;
  36import eu.siacs.conversations.xml.Element;
  37import eu.siacs.conversations.xmpp.OnIqPacketReceived;
  38import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
  39import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
  40import eu.siacs.conversations.xmpp.OnStatusChanged;
  41import eu.siacs.conversations.xmpp.OnTLSExceptionReceived;
  42import eu.siacs.conversations.xmpp.XmppConnection;
  43import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  44import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  45import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  46import android.app.AlarmManager;
  47import android.app.PendingIntent;
  48import android.app.Service;
  49import android.content.Context;
  50import android.content.Intent;
  51import android.content.SharedPreferences;
  52import android.database.ContentObserver;
  53import android.database.DatabaseUtils;
  54import android.net.ConnectivityManager;
  55import android.net.NetworkInfo;
  56import android.os.Binder;
  57import android.os.Bundle;
  58import android.os.IBinder;
  59import android.os.PowerManager;
  60import android.os.SystemClock;
  61import android.preference.PreferenceManager;
  62import android.provider.ContactsContract;
  63import android.util.Log;
  64
  65public class XmppConnectionService extends Service {
  66
  67	protected static final String LOGTAG = "xmppService";
  68	public DatabaseBackend databaseBackend;
  69
  70	public long startDate;
  71
  72	private static final int PING_MAX_INTERVAL = 300;
  73	private static final int PING_MIN_INTERVAL = 10;
  74	private static final int PING_TIMEOUT = 2;
  75
  76	private List<Account> accounts;
  77	private List<Conversation> conversations = null;
  78
  79	public OnConversationListChangedListener convChangedListener = null;
  80	private OnAccountListChangedListener accountChangedListener = null;
  81	private OnTLSExceptionReceived tlsException = null;
  82	
  83	public void setOnTLSExceptionReceivedListener(
  84			OnTLSExceptionReceived listener) {
  85		tlsException = listener;
  86	}
  87
  88	private Random mRandom = new Random(System.currentTimeMillis());
  89
  90	private ContentObserver contactObserver = new ContentObserver(null) {
  91		@Override
  92		public void onChange(boolean selfChange) {
  93			super.onChange(selfChange);
  94			Log.d(LOGTAG, "contact list has changed");
  95			mergePhoneContactsWithRoster(null);
  96		}
  97	};
  98
  99	private XmppConnectionService service = this;
 100
 101	private final IBinder mBinder = new XmppConnectionBinder();
 102	private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() {
 103
 104		@Override
 105		public void onMessagePacketReceived(Account account,
 106				MessagePacket packet) {
 107			Message message = null;
 108			boolean notify = true;
 109			if ((packet.getType() == MessagePacket.TYPE_CHAT)) {
 110				String pgpBody = MessageParser.getPgpBody(packet);
 111				if (pgpBody != null) {
 112					message = MessageParser.parsePgpChat(pgpBody, packet,
 113							account, service);
 114					message.markUnread();
 115				} else if (packet.hasChild("body")
 116						&& (packet.getBody().startsWith("?OTR"))) {
 117					message = MessageParser.parseOtrChat(packet, account,
 118							service);
 119					if (message != null) {
 120						message.markUnread();
 121					}
 122				} else if (packet.hasChild("body")) {
 123					message = MessageParser.parsePlainTextChat(packet, account,
 124							service);
 125					message.markUnread();
 126				} else if (packet.hasChild("received")
 127						|| (packet.hasChild("sent"))) {
 128					message = MessageParser.parseCarbonMessage(packet, account,
 129							service);
 130					if (message != null) {
 131						message.getConversation().markRead();
 132					}
 133					notify = false;
 134				}
 135
 136			} else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) {
 137				message = MessageParser
 138						.parseGroupchat(packet, account, service);
 139				if (message != null) {
 140					if (message.getStatus() == Message.STATUS_RECIEVED) {
 141						message.markUnread();
 142					} else {
 143						message.getConversation().markRead();
 144						notify = false;
 145					}
 146				}
 147			} else if (packet.getType() == MessagePacket.TYPE_ERROR) {
 148				message = MessageParser.parseError(packet, account, service);
 149			} else {
 150				// Log.d(LOGTAG, "unparsed message " + packet.toString());
 151			}
 152			if (message == null) {
 153				return;
 154			}
 155			if (packet.hasChild("delay")) {
 156				try {
 157					String stamp = packet.findChild("delay").getAttribute(
 158							"stamp");
 159					stamp = stamp.replace("Z", "+0000");
 160					Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
 161							.parse(stamp);
 162					message.setTime(date.getTime());
 163				} catch (ParseException e) {
 164					Log.d(LOGTAG, "error trying to parse date" + e.getMessage());
 165				}
 166			}
 167			Conversation conversation = message.getConversation();
 168			conversation.getMessages().add(message);
 169			if (packet.getType() != MessagePacket.TYPE_ERROR) {
 170				databaseBackend.createMessage(message);
 171			}
 172			if (convChangedListener != null) {
 173				convChangedListener.onConversationListChanged();
 174			} else {
 175				UIHelper.updateNotification(getApplicationContext(),
 176						getConversations(), notify);
 177			}
 178		}
 179	};
 180	private OnStatusChanged statusListener = new OnStatusChanged() {
 181
 182		@Override
 183		public void onStatusChanged(Account account) {
 184			if (accountChangedListener != null) {
 185				accountChangedListener.onAccountListChangedListener();
 186			}
 187			if (account.getStatus() == Account.STATUS_ONLINE) {
 188				databaseBackend.clearPresences(account);
 189				if (account.getXmppConnection().hasFeatureRosterManagment()) {
 190					updateRoster(account, null);
 191				}
 192				connectMultiModeConversations(account);
 193				List<Conversation> conversations = getConversations();
 194				for (int i = 0; i < conversations.size(); ++i) {
 195					if (conversations.get(i).getAccount() == account) {
 196						sendUnsendMessages(conversations.get(i));
 197					}
 198				}
 199				if (convChangedListener != null) {
 200					convChangedListener.onConversationListChanged();
 201				}
 202				scheduleWakeupCall(PING_MAX_INTERVAL, true);
 203			} else if (account.getStatus() == Account.STATUS_OFFLINE) {
 204				databaseBackend.clearPresences(account);
 205				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 206					int timeToReconnect = mRandom.nextInt(50) + 10;
 207					scheduleWakeupCall(timeToReconnect, false);
 208				}
 209
 210			}
 211		}
 212	};
 213
 214	private OnPresencePacketReceived presenceListener = new OnPresencePacketReceived() {
 215
 216		@Override
 217		public void onPresencePacketReceived(Account account,
 218				PresencePacket packet) {
 219			if (packet.hasChild("x")
 220					&& (packet.findChild("x").getAttribute("xmlns")
 221							.startsWith("http://jabber.org/protocol/muc"))) {
 222				Conversation muc = findMuc(packet.getAttribute("from").split(
 223						"/")[0]);
 224				if (muc != null) {
 225					int error = muc.getMucOptions().getError();
 226					muc.getMucOptions().processPacket(packet);
 227					if ((muc.getMucOptions().getError() != error)
 228							&& (convChangedListener != null)) {
 229						Log.d(LOGTAG, "muc error status changed");
 230						convChangedListener.onConversationListChanged();
 231					}
 232				}
 233			} else {
 234				String[] fromParts = packet.getAttribute("from").split("/");
 235				Contact contact = findContact(account, fromParts[0]);
 236				if (contact == null) {
 237					// most likely self or roster not synced
 238					return;
 239				}
 240				String type = packet.getAttribute("type");
 241				if (type == null) {
 242					Element show = packet.findChild("show");
 243					if (show == null) {
 244						contact.updatePresence(fromParts[1], Presences.ONLINE);
 245					} else if (show.getContent().equals("away")) {
 246						contact.updatePresence(fromParts[1], Presences.AWAY);
 247					} else if (show.getContent().equals("xa")) {
 248						contact.updatePresence(fromParts[1], Presences.XA);
 249					} else if (show.getContent().equals("chat")) {
 250						contact.updatePresence(fromParts[1], Presences.CHAT);
 251					} else if (show.getContent().equals("dnd")) {
 252						contact.updatePresence(fromParts[1], Presences.DND);
 253					}
 254					PgpEngine pgp = getPgpEngine();
 255					if (pgp != null) {
 256						Element x = packet.findChild("x");
 257						if ((x != null)
 258								&& (x.getAttribute("xmlns")
 259										.equals("jabber:x:signed"))) {
 260							try {
 261								contact.setPgpKeyId(pgp.fetchKeyId(packet
 262										.findChild("status").getContent(), x
 263										.getContent()));
 264							} catch (OpenPgpException e) {
 265								Log.d(LOGTAG, "faulty pgp. just ignore");
 266							}
 267						}
 268					}
 269					databaseBackend.updateContact(contact);
 270				} else if (type.equals("unavailable")) {
 271					if (fromParts.length != 2) {
 272						// Log.d(LOGTAG,"received presence with no resource "+packet.toString());
 273					} else {
 274						contact.removePresence(fromParts[1]);
 275						databaseBackend.updateContact(contact);
 276					}
 277				} else if (type.equals("subscribe")) {
 278					if (contact
 279							.getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) {
 280						sendPresenceUpdatesTo(contact);
 281						contact.setSubscriptionOption(Contact.Subscription.FROM);
 282						contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT);
 283						replaceContactInConversation(contact.getJid(), contact);
 284						databaseBackend.updateContact(contact);
 285						if ((contact
 286								.getSubscriptionOption(Contact.Subscription.ASKING))
 287								&& (!contact
 288										.getSubscriptionOption(Contact.Subscription.TO))) {
 289							requestPresenceUpdatesFrom(contact);
 290						}
 291					} else {
 292						// TODO: ask user to handle it maybe
 293					}
 294				} else {
 295					Log.d(LOGTAG, packet.toString());
 296				}
 297				replaceContactInConversation(contact.getJid(), contact);
 298			}
 299		}
 300	};
 301
 302	private OnIqPacketReceived unknownIqListener = new OnIqPacketReceived() {
 303
 304		@Override
 305		public void onIqPacketReceived(Account account, IqPacket packet) {
 306			if (packet.hasChild("query")) {
 307				Element query = packet.findChild("query");
 308				String xmlns = query.getAttribute("xmlns");
 309				if ((xmlns != null) && (xmlns.equals("jabber:iq:roster"))) {
 310					processRosterItems(account, query);
 311					mergePhoneContactsWithRoster(null);
 312				}
 313			}
 314		}
 315	};
 316
 317	private OpenPgpServiceConnection pgpServiceConnection;
 318	private PgpEngine mPgpEngine = null;
 319	private Intent pingIntent;
 320	private PendingIntent pendingPingIntent = null;
 321
 322	public PgpEngine getPgpEngine() {
 323		if (pgpServiceConnection.isBound()) {
 324			if (this.mPgpEngine == null) {
 325				this.mPgpEngine = new PgpEngine(new OpenPgpApi(
 326						getApplicationContext(),
 327						pgpServiceConnection.getService()));
 328			}
 329			return mPgpEngine;
 330		} else {
 331			return null;
 332		}
 333
 334	}
 335
 336	protected Conversation findMuc(String name) {
 337		for (Conversation conversation : this.conversations) {
 338			if (conversation.getContactJid().split("/")[0].equals(name)) {
 339				return conversation;
 340			}
 341		}
 342		return null;
 343	}
 344
 345	private void processRosterItems(Account account, Element elements) {
 346		String version = elements.getAttribute("ver");
 347		if (version != null) {
 348			account.setRosterVersion(version);
 349			databaseBackend.updateAccount(account);
 350		}
 351		for (Element item : elements.getChildren()) {
 352			if (item.getName().equals("item")) {
 353				String jid = item.getAttribute("jid");
 354				String subscription = item.getAttribute("subscription");
 355				Contact contact = databaseBackend.findContact(account, jid);
 356				if (contact == null) {
 357					if (!subscription.equals("remove")) {
 358						String name = item.getAttribute("name");
 359						if (name == null) {
 360							name = jid.split("@")[0];
 361						}
 362						contact = new Contact(account, name, jid, null);
 363						contact.parseSubscriptionFromElement(item);
 364						databaseBackend.createContact(contact);
 365					}
 366				} else {
 367					if (subscription.equals("remove")) {
 368						databaseBackend.deleteContact(contact);
 369						replaceContactInConversation(contact.getJid(), null);
 370					} else {
 371						contact.parseSubscriptionFromElement(item);
 372						databaseBackend.updateContact(contact);
 373						replaceContactInConversation(contact.getJid(), contact);
 374					}
 375				}
 376			}
 377		}
 378	}
 379
 380	private void replaceContactInConversation(String jid, Contact contact) {
 381		List<Conversation> conversations = getConversations();
 382		for (int i = 0; i < conversations.size(); ++i) {
 383			if ((conversations.get(i).getContactJid().equals(jid))) {
 384				conversations.get(i).setContact(contact);
 385				break;
 386			}
 387		}
 388	}
 389
 390	public class XmppConnectionBinder extends Binder {
 391		public XmppConnectionService getService() {
 392			return XmppConnectionService.this;
 393		}
 394	}
 395
 396	@Override
 397	public int onStartCommand(Intent intent, int flags, int startId) {
 398		Log.d(LOGTAG,"calling start service. caller was:"+intent.getAction());
 399		
 400		// internet and online last_received - list_ping >= max_ping : ping
 401		// internet and online last_ping - last_received >= ping_timeout :
 402		// reconnect
 403		// internet and offline and enabled : connect (Threat start)
 404
 405		// no internet - set no internet
 406
 407		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
 408				.getSystemService(Context.CONNECTIVITY_SERVICE);
 409		NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
 410		boolean isConnected = activeNetwork != null
 411				&& activeNetwork.isConnected();
 412
 413		for (Account account : accounts) {
 414			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 415				if (!isConnected) {
 416					account.setStatus(Account.STATUS_NO_INTERNET);
 417				} else {
 418					if (account.getStatus() == Account.STATUS_NO_INTERNET) {
 419						account.setStatus(Account.STATUS_OFFLINE);
 420					}
 421
 422					// TODO 3 remaining cases
 423					if (account.getStatus() == Account.STATUS_ONLINE) {
 424						long lastReceived = account.getXmppConnection().lastPaketReceived;
 425						long lastSent = account.getXmppConnection().lastPingSent;
 426						if (lastSent - lastReceived >= PING_TIMEOUT * 1000) {
 427							Log.d(LOGTAG, account.getJid() + ": ping timeout");
 428							this.reconnectAccount(account);
 429						} else if (SystemClock.elapsedRealtime() - lastReceived >= PING_MIN_INTERVAL * 1000) {
 430							account.getXmppConnection().sendPing();
 431							account.getXmppConnection().lastPingSent = SystemClock.elapsedRealtime();
 432							this.scheduleWakeupCall(2, false);
 433						}
 434					} else if (account.getStatus() == Account.STATUS_OFFLINE) {
 435						if (account.getXmppConnection() == null) {
 436							account.setXmppConnection(this
 437									.createConnection(account));
 438						}
 439						account.getXmppConnection().lastPingSent = SystemClock.elapsedRealtime();
 440						new Thread(account.getXmppConnection()).start();
 441					} else {
 442						Log.d(LOGTAG,account.getJid()+": status="+account.getStatus());
 443					}
 444					//in any case. reschedule wakup call
 445					this.scheduleWakeupCall(PING_MAX_INTERVAL, true);
 446				}
 447				if (accountChangedListener != null) {
 448					accountChangedListener.onAccountListChangedListener();
 449				}
 450			}
 451		}
 452		return START_STICKY;
 453	}
 454
 455	@Override
 456	public void onCreate() {
 457		ExceptionHelper.init(getApplicationContext());
 458		databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
 459		this.accounts = databaseBackend.getAccounts();
 460
 461		getContentResolver().registerContentObserver(
 462				ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
 463		this.pgpServiceConnection = new OpenPgpServiceConnection(
 464				getApplicationContext(), "org.sufficientlysecure.keychain");
 465		this.pgpServiceConnection.bindToService();
 466
 467	}
 468
 469	@Override
 470	public void onDestroy() {
 471		super.onDestroy();
 472		for (Account account : accounts) {
 473			if (account.getXmppConnection() != null) {
 474				disconnect(account, true);
 475			}
 476		}
 477	}
 478
 479	protected void scheduleWakeupCall(int seconds, boolean ping) {
 480		long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
 481		Context context = getApplicationContext();
 482		AlarmManager alarmManager = (AlarmManager) context
 483				.getSystemService(Context.ALARM_SERVICE);
 484		
 485		
 486		
 487		if (ping) {
 488			if (this.pingIntent==null) {
 489				this.pingIntent = new Intent(context, EventReceiver.class);
 490				this.pingIntent.setAction("ping");
 491				this.pingIntent.putExtra("time", timeToWake);
 492				this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
 493						this.pingIntent, 0);
 494				alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,timeToWake, pendingPingIntent);
 495				//Log.d(LOGTAG,"schedule ping in "+seconds+" seconds");
 496			} else {
 497				long scheduledTime = this.pingIntent.getLongExtra("time", 0);
 498				if (scheduledTime<SystemClock.elapsedRealtime() || (scheduledTime > timeToWake)) {
 499					this.pingIntent.putExtra("time", timeToWake);
 500					alarmManager.cancel(this.pendingPingIntent);
 501					this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
 502							this.pingIntent, 0);
 503					alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,timeToWake, pendingPingIntent);
 504					//Log.d(LOGTAG,"reschedule old ping to ping in "+seconds+" seconds");
 505				}
 506			}
 507		} else {
 508			Intent intent = new Intent(context, EventReceiver.class);
 509			intent.setAction("ping_check");
 510			PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
 511					intent, 0);
 512			alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,timeToWake, alarmIntent);
 513		}
 514
 515	}
 516
 517	public XmppConnection createConnection(Account account) {
 518		PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 519		XmppConnection connection = new XmppConnection(account, pm);
 520		connection.setOnMessagePacketReceivedListener(this.messageListener);
 521		connection.setOnStatusChangedListener(this.statusListener);
 522		connection.setOnPresencePacketReceivedListener(this.presenceListener);
 523		connection
 524				.setOnUnregisteredIqPacketReceivedListener(this.unknownIqListener);
 525		connection
 526				.setOnTLSExceptionReceivedListener(new OnTLSExceptionReceived() {
 527
 528					@Override
 529					public void onTLSExceptionReceived(String fingerprint,
 530							Account account) {
 531						Log.d(LOGTAG, "tls exception arrived in service");
 532						if (tlsException != null) {
 533							tlsException.onTLSExceptionReceived(fingerprint,
 534									account);
 535						}
 536					}
 537				});
 538		return connection;
 539	}
 540
 541	public void sendMessage(Message message, String presence) {
 542		Account account = message.getConversation().getAccount();
 543		Conversation conv = message.getConversation();
 544		boolean saveInDb = false;
 545		boolean addToConversation = false;
 546		if (account.getStatus() == Account.STATUS_ONLINE) {
 547			MessagePacket packet;
 548			if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 549				if (!conv.hasValidOtrSession()) {
 550					// starting otr session. messages will be send later
 551					conv.startOtrSession(getApplicationContext(), presence);
 552				} else if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 553					// otr session aleary exists, creating message packet
 554					// accordingly
 555					packet = prepareMessagePacket(account, message,
 556							conv.getOtrSession());
 557					account.getXmppConnection().sendMessagePacket(packet);
 558					message.setStatus(Message.STATUS_SEND);
 559				}
 560				saveInDb = true;
 561				addToConversation = true;
 562			} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 563				long keyId = message.getConversation().getContact()
 564						.getPgpKeyId();
 565				packet = new MessagePacket();
 566				packet.setType(MessagePacket.TYPE_CHAT);
 567				packet.setFrom(message.getConversation().getAccount()
 568						.getFullJid());
 569				packet.setTo(message.getCounterpart());
 570				packet.setBody("This is an XEP-0027 encryted message");
 571				Element x = new Element("x");
 572				x.setAttribute("xmlns", "jabber:x:encrypted");
 573				x.setContent(this.getPgpEngine().encrypt(keyId,
 574						message.getBody()));
 575				packet.addChild(x);
 576				account.getXmppConnection().sendMessagePacket(packet);
 577				message.setStatus(Message.STATUS_SEND);
 578				message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 579				saveInDb = true;
 580				addToConversation = true;
 581			} else {
 582				// don't encrypt
 583				if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
 584					message.setStatus(Message.STATUS_SEND);
 585					saveInDb = true;
 586					addToConversation = true;
 587				}
 588
 589				packet = prepareMessagePacket(account, message, null);
 590				account.getXmppConnection().sendMessagePacket(packet);
 591			}
 592		} else {
 593			// account is offline
 594			saveInDb = true;
 595			addToConversation = true;
 596
 597		}
 598		if (saveInDb) {
 599			databaseBackend.createMessage(message);
 600		}
 601		if (addToConversation) {
 602			conv.getMessages().add(message);
 603			if (convChangedListener != null) {
 604				convChangedListener.onConversationListChanged();
 605			}
 606		}
 607
 608	}
 609
 610	private void sendUnsendMessages(Conversation conversation) {
 611		for (int i = 0; i < conversation.getMessages().size(); ++i) {
 612			if (conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND) {
 613				Message message = conversation.getMessages().get(i);
 614				MessagePacket packet = prepareMessagePacket(
 615						conversation.getAccount(), message, null);
 616				conversation.getAccount().getXmppConnection()
 617						.sendMessagePacket(packet);
 618				message.setStatus(Message.STATUS_SEND);
 619				if (conversation.getMode() == Conversation.MODE_SINGLE) {
 620					databaseBackend.updateMessage(message);
 621				} else {
 622					databaseBackend.deleteMessage(message);
 623					conversation.getMessages().remove(i);
 624					i--;
 625				}
 626			}
 627		}
 628	}
 629
 630	public MessagePacket prepareMessagePacket(Account account, Message message,
 631			Session otrSession) {
 632		MessagePacket packet = new MessagePacket();
 633		if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
 634			packet.setType(MessagePacket.TYPE_CHAT);
 635			if (otrSession != null) {
 636				try {
 637					packet.setBody(otrSession.transformSending(message
 638							.getBody()));
 639				} catch (OtrException e) {
 640					Log.d(LOGTAG,
 641							account.getJid()
 642									+ ": could not encrypt message to "
 643									+ message.getCounterpart());
 644				}
 645				Element privateMarker = new Element("private");
 646				privateMarker.setAttribute("xmlns", "urn:xmpp:carbons:2");
 647				packet.addChild(privateMarker);
 648				packet.setTo(otrSession.getSessionID().getAccountID() + "/"
 649						+ otrSession.getSessionID().getUserID());
 650				packet.setFrom(account.getFullJid());
 651			} else {
 652				packet.setBody(message.getBody());
 653				packet.setTo(message.getCounterpart());
 654				packet.setFrom(account.getJid());
 655			}
 656		} else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 657			packet.setType(MessagePacket.TYPE_GROUPCHAT);
 658			packet.setBody(message.getBody());
 659			packet.setTo(message.getCounterpart().split("/")[0]);
 660			packet.setFrom(account.getJid());
 661		}
 662		return packet;
 663	}
 664
 665	public void getRoster(Account account,
 666			final OnRosterFetchedListener listener) {
 667		List<Contact> contacts = databaseBackend.getContactsByAccount(account);
 668		for (int i = 0; i < contacts.size(); ++i) {
 669			contacts.get(i).setAccount(account);
 670		}
 671		if (listener != null) {
 672			listener.onRosterFetched(contacts);
 673		}
 674	}
 675
 676	public void updateRoster(final Account account,
 677			final OnRosterFetchedListener listener) {
 678		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
 679		Element query = new Element("query");
 680		query.setAttribute("xmlns", "jabber:iq:roster");
 681		if (!"".equals(account.getRosterVersion())) {
 682			Log.d(LOGTAG, account.getJid() + ": fetching roster version "
 683					+ account.getRosterVersion());
 684		} else {
 685			Log.d(LOGTAG, account.getJid() + ": fetching roster");
 686		}
 687		query.setAttribute("ver", account.getRosterVersion());
 688		iqPacket.addChild(query);
 689		account.getXmppConnection().sendIqPacket(iqPacket,
 690				new OnIqPacketReceived() {
 691
 692					@Override
 693					public void onIqPacketReceived(final Account account,
 694							IqPacket packet) {
 695						Element roster = packet.findChild("query");
 696						if (roster != null) {
 697							Log.d(LOGTAG, account.getJid()
 698									+ ": processing roster");
 699							processRosterItems(account, roster);
 700							StringBuilder mWhere = new StringBuilder();
 701							mWhere.append("jid NOT IN(");
 702							List<Element> items = roster.getChildren();
 703							for (int i = 0; i < items.size(); ++i) {
 704								mWhere.append(DatabaseUtils
 705										.sqlEscapeString(items.get(i)
 706												.getAttribute("jid")));
 707								if (i != items.size() - 1) {
 708									mWhere.append(",");
 709								}
 710							}
 711							mWhere.append(") and accountUuid = \"");
 712							mWhere.append(account.getUuid());
 713							mWhere.append("\"");
 714							List<Contact> contactsToDelete = databaseBackend
 715									.getContacts(mWhere.toString());
 716							for (Contact contact : contactsToDelete) {
 717								databaseBackend.deleteContact(contact);
 718								replaceContactInConversation(contact.getJid(),
 719										null);
 720							}
 721
 722						} else {
 723							Log.d(LOGTAG, account.getJid()
 724									+ ": empty roster returend");
 725						}
 726						mergePhoneContactsWithRoster(new OnPhoneContactsMerged() {
 727
 728							@Override
 729							public void phoneContactsMerged() {
 730								if (listener != null) {
 731									getRoster(account, listener);
 732								}
 733							}
 734						});
 735					}
 736				});
 737	}
 738
 739	public void mergePhoneContactsWithRoster(
 740			final OnPhoneContactsMerged listener) {
 741		PhoneHelper.loadPhoneContacts(getApplicationContext(),
 742				new OnPhoneContactsLoadedListener() {
 743					@Override
 744					public void onPhoneContactsLoaded(
 745							Hashtable<String, Bundle> phoneContacts) {
 746						List<Contact> contacts = databaseBackend
 747								.getContactsByAccount(null);
 748						for (int i = 0; i < contacts.size(); ++i) {
 749							Contact contact = contacts.get(i);
 750							if (phoneContacts.containsKey(contact.getJid())) {
 751								Bundle phoneContact = phoneContacts.get(contact
 752										.getJid());
 753								String systemAccount = phoneContact
 754										.getInt("phoneid")
 755										+ "#"
 756										+ phoneContact.getString("lookup");
 757								contact.setSystemAccount(systemAccount);
 758								contact.setPhotoUri(phoneContact
 759										.getString("photouri"));
 760								contact.setDisplayName(phoneContact
 761										.getString("displayname"));
 762								databaseBackend.updateContact(contact);
 763								replaceContactInConversation(contact.getJid(),
 764										contact);
 765							} else {
 766								if ((contact.getSystemAccount() != null)
 767										|| (contact.getProfilePhoto() != null)) {
 768									contact.setSystemAccount(null);
 769									contact.setPhotoUri(null);
 770									databaseBackend.updateContact(contact);
 771									replaceContactInConversation(
 772											contact.getJid(), contact);
 773								}
 774							}
 775						}
 776						if (listener != null) {
 777							listener.phoneContactsMerged();
 778						}
 779					}
 780				});
 781	}
 782
 783	public List<Conversation> getConversations() {
 784		if (this.conversations == null) {
 785			Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
 786			for (Account account : this.accounts) {
 787				accountLookupTable.put(account.getUuid(), account);
 788			}
 789			this.conversations = databaseBackend
 790					.getConversations(Conversation.STATUS_AVAILABLE);
 791			for (Conversation conv : this.conversations) {
 792				Account account = accountLookupTable.get(conv.getAccountUuid());
 793				conv.setAccount(account);
 794				conv.setContact(findContact(account, conv.getContactJid()));
 795				conv.setMessages(databaseBackend.getMessages(conv, 50));
 796			}
 797		}
 798		return this.conversations;
 799	}
 800
 801	public List<Account> getAccounts() {
 802		return this.accounts;
 803	}
 804
 805	public Contact findContact(Account account, String jid) {
 806		Contact contact = databaseBackend.findContact(account, jid);
 807		if (contact != null) {
 808			contact.setAccount(account);
 809		}
 810		return contact;
 811	}
 812
 813	public Conversation findOrCreateConversation(Account account, String jid,
 814			boolean muc) {
 815		for (Conversation conv : this.getConversations()) {
 816			if ((conv.getAccount().equals(account))
 817					&& (conv.getContactJid().split("/")[0].equals(jid))) {
 818				return conv;
 819			}
 820		}
 821		Conversation conversation = databaseBackend.findConversation(account,
 822				jid);
 823		if (conversation != null) {
 824			conversation.setStatus(Conversation.STATUS_AVAILABLE);
 825			conversation.setAccount(account);
 826			if (muc) {
 827				conversation.setMode(Conversation.MODE_MULTI);
 828				if (account.getStatus() == Account.STATUS_ONLINE) {
 829					joinMuc(conversation);
 830				}
 831			} else {
 832				conversation.setMode(Conversation.MODE_SINGLE);
 833			}
 834			this.databaseBackend.updateConversation(conversation);
 835			conversation.setContact(findContact(account,
 836					conversation.getContactJid()));
 837		} else {
 838			String conversationName;
 839			Contact contact = findContact(account, jid);
 840			if (contact != null) {
 841				conversationName = contact.getDisplayName();
 842			} else {
 843				conversationName = jid.split("@")[0];
 844			}
 845			if (muc) {
 846				conversation = new Conversation(conversationName, account, jid,
 847						Conversation.MODE_MULTI);
 848				if (account.getStatus() == Account.STATUS_ONLINE) {
 849					joinMuc(conversation);
 850				}
 851			} else {
 852				conversation = new Conversation(conversationName, account, jid,
 853						Conversation.MODE_SINGLE);
 854			}
 855			conversation.setContact(contact);
 856			this.databaseBackend.createConversation(conversation);
 857		}
 858		this.conversations.add(conversation);
 859		if (this.convChangedListener != null) {
 860			this.convChangedListener.onConversationListChanged();
 861		}
 862		return conversation;
 863	}
 864
 865	public void archiveConversation(Conversation conversation) {
 866		if (conversation.getMode() == Conversation.MODE_MULTI) {
 867			leaveMuc(conversation);
 868		} else {
 869			try {
 870				conversation.endOtrIfNeeded();
 871			} catch (OtrException e) {
 872				Log.d(LOGTAG,
 873						"error ending otr session for "
 874								+ conversation.getName());
 875			}
 876		}
 877		this.databaseBackend.updateConversation(conversation);
 878		this.conversations.remove(conversation);
 879		if (this.convChangedListener != null) {
 880			this.convChangedListener.onConversationListChanged();
 881		}
 882	}
 883
 884	public int getConversationCount() {
 885		return this.databaseBackend.getConversationCount();
 886	}
 887
 888	public void createAccount(Account account) {
 889		databaseBackend.createAccount(account);
 890		this.accounts.add(account);
 891		account.setXmppConnection(this.createConnection(account));
 892		if (accountChangedListener != null)
 893			accountChangedListener.onAccountListChangedListener();
 894	}
 895
 896	public void deleteContact(Contact contact) {
 897		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
 898		Element query = new Element("query");
 899		query.setAttribute("xmlns", "jabber:iq:roster");
 900		Element item = new Element("item");
 901		item.setAttribute("jid", contact.getJid());
 902		item.setAttribute("subscription", "remove");
 903		query.addChild(item);
 904		iq.addChild(query);
 905		contact.getAccount().getXmppConnection().sendIqPacket(iq, null);
 906		replaceContactInConversation(contact.getJid(), null);
 907		databaseBackend.deleteContact(contact);
 908	}
 909
 910	public void updateAccount(Account account) {
 911		databaseBackend.updateAccount(account);
 912		reconnectAccount(account);
 913		if (accountChangedListener != null)
 914			accountChangedListener.onAccountListChangedListener();
 915	}
 916
 917	public void deleteAccount(Account account) {
 918		Log.d(LOGTAG, "called delete account");
 919		if (account.getXmppConnection() != null) {
 920			this.disconnect(account, false);
 921		}
 922		databaseBackend.deleteAccount(account);
 923		this.accounts.remove(account);
 924		if (accountChangedListener != null)
 925			accountChangedListener.onAccountListChangedListener();
 926	}
 927
 928	public void setOnConversationListChangedListener(
 929			OnConversationListChangedListener listener) {
 930		this.convChangedListener = listener;
 931	}
 932
 933	public void removeOnConversationListChangedListener() {
 934		this.convChangedListener = null;
 935	}
 936
 937	public void setOnAccountListChangedListener(
 938			OnAccountListChangedListener listener) {
 939		this.accountChangedListener = listener;
 940	}
 941
 942	public void removeOnAccountListChangedListener() {
 943		this.accountChangedListener = null;
 944	}
 945
 946	public void connectMultiModeConversations(Account account) {
 947		List<Conversation> conversations = getConversations();
 948		for (int i = 0; i < conversations.size(); i++) {
 949			Conversation conversation = conversations.get(i);
 950			if ((conversation.getMode() == Conversation.MODE_MULTI)
 951					&& (conversation.getAccount() == account)) {
 952				joinMuc(conversation);
 953			}
 954		}
 955	}
 956
 957	public void joinMuc(Conversation conversation) {
 958		String[] mucParts = conversation.getContactJid().split("/");
 959		String muc;
 960		String nick;
 961		if (mucParts.length == 2) {
 962			muc = mucParts[0];
 963			nick = mucParts[1];
 964		} else {
 965			muc = mucParts[0];
 966			nick = conversation.getAccount().getUsername();
 967		}
 968		PresencePacket packet = new PresencePacket();
 969		packet.setAttribute("to", muc + "/" + nick);
 970		Element x = new Element("x");
 971		x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
 972		if (conversation.getMessages().size() != 0) {
 973			Element history = new Element("history");
 974			long lastMsgTime = conversation.getLatestMessage().getTimeSent();
 975			long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1;
 976			history.setAttribute("seconds", diff + "");
 977			x.addChild(history);
 978		}
 979		packet.addChild(x);
 980		conversation.getAccount().getXmppConnection()
 981				.sendPresencePacket(packet);
 982	}
 983
 984	private OnRenameListener renameListener = null;
 985	private boolean pongReceived;
 986
 987	public void setOnRenameListener(OnRenameListener listener) {
 988		this.renameListener = listener;
 989	}
 990
 991	public void renameInMuc(final Conversation conversation, final String nick) {
 992		final MucOptions options = conversation.getMucOptions();
 993		if (options.online()) {
 994			options.setOnRenameListener(new OnRenameListener() {
 995
 996				@Override
 997				public void onRename(boolean success) {
 998					if (renameListener != null) {
 999						renameListener.onRename(success);
1000					}
1001					if (success) {
1002						databaseBackend.updateConversation(conversation);
1003					}
1004				}
1005			});
1006			PresencePacket packet = new PresencePacket();
1007			packet.setAttribute("to",
1008					conversation.getContactJid().split("/")[0] + "/" + nick);
1009			packet.setAttribute("from", conversation.getAccount().getFullJid());
1010
1011			conversation.getAccount().getXmppConnection()
1012					.sendPresencePacket(packet, new OnPresencePacketReceived() {
1013
1014						@Override
1015						public void onPresencePacketReceived(Account account,
1016								PresencePacket packet) {
1017							final boolean changed;
1018							String type = packet.getAttribute("type");
1019							changed = (!"error".equals(type));
1020							if (!changed) {
1021								options.getOnRenameListener().onRename(false);
1022							} else {
1023								if (type == null) {
1024									options.getOnRenameListener()
1025											.onRename(true);
1026									options.setNick(packet.getAttribute("from")
1027											.split("/")[1]);
1028								} else {
1029									options.processPacket(packet);
1030								}
1031							}
1032						}
1033					});
1034		} else {
1035			String jid = conversation.getContactJid().split("/")[0] + "/"
1036					+ nick;
1037			conversation.setContactJid(jid);
1038			databaseBackend.updateConversation(conversation);
1039			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1040				joinMuc(conversation);
1041			}
1042		}
1043	}
1044
1045	public void leaveMuc(Conversation conversation) {
1046		PresencePacket packet = new PresencePacket();
1047		packet.setAttribute("to", conversation.getContactJid());
1048		packet.setAttribute("from", conversation.getAccount().getFullJid());
1049		packet.setAttribute("type", "unavailable");
1050		conversation.getAccount().getXmppConnection()
1051				.sendPresencePacket(packet);
1052		conversation.getMucOptions().setOffline();
1053	}
1054
1055	public void disconnect(final Account account, boolean blocking) {
1056		if (account.getStatus() == Account.STATUS_ONLINE) {
1057			List<Conversation> conversations = getConversations();
1058			for (int i = 0; i < conversations.size(); i++) {
1059				Conversation conversation = conversations.get(i);
1060				if (conversation.getAccount() == account) {
1061					if (conversation.getMode() == Conversation.MODE_MULTI) {
1062						leaveMuc(conversation);
1063					} else {
1064						try {
1065							conversation.endOtrIfNeeded();
1066						} catch (OtrException e) {
1067							Log.d(LOGTAG, "error ending otr session for "
1068									+ conversation.getName());
1069						}
1070					}
1071				}
1072			}
1073			if (!blocking) {
1074				new Thread(new Runnable() {
1075	
1076					@Override
1077					public void run() {
1078						account.getXmppConnection().disconnect(false);
1079						Log.d(LOGTAG, "disconnected account: " + account.getJid());
1080						//account.setXmppConnection(null);
1081					}
1082				}).start();
1083			} else {
1084				account.getXmppConnection().disconnect(false);
1085				Log.d(LOGTAG, "disconnected account: " + account.getJid());
1086				//account.setXmppConnection(null);
1087			}
1088		}
1089	}
1090
1091	@Override
1092	public IBinder onBind(Intent intent) {
1093		return mBinder;
1094	}
1095
1096	public void updateContact(Contact contact) {
1097		databaseBackend.updateContact(contact);
1098		replaceContactInConversation(contact.getJid(), contact);
1099	}
1100
1101	public void updateMessage(Message message) {
1102		databaseBackend.updateMessage(message);
1103	}
1104
1105	public void createContact(Contact contact) {
1106		SharedPreferences sharedPref = PreferenceManager
1107				.getDefaultSharedPreferences(getApplicationContext());
1108		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1109		if (autoGrant) {
1110			contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT);
1111			contact.setSubscriptionOption(Contact.Subscription.ASKING);
1112		}
1113		databaseBackend.createContact(contact);
1114		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1115		Element query = new Element("query");
1116		query.setAttribute("xmlns", "jabber:iq:roster");
1117		Element item = new Element("item");
1118		item.setAttribute("jid", contact.getJid());
1119		item.setAttribute("name", contact.getJid());
1120		query.addChild(item);
1121		iq.addChild(query);
1122		Account account = contact.getAccount();
1123		account.getXmppConnection().sendIqPacket(iq, null);
1124		if (autoGrant) {
1125			requestPresenceUpdatesFrom(contact);
1126		}
1127		replaceContactInConversation(contact.getJid(), contact);
1128	}
1129
1130	public void requestPresenceUpdatesFrom(Contact contact) {
1131		// Requesting a Subscription type=subscribe
1132		PresencePacket packet = new PresencePacket();
1133		packet.setAttribute("type", "subscribe");
1134		packet.setAttribute("to", contact.getJid());
1135		packet.setAttribute("from", contact.getAccount().getJid());
1136		Log.d(LOGTAG, packet.toString());
1137		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1138	}
1139
1140	public void stopPresenceUpdatesFrom(Contact contact) {
1141		// Unsubscribing type='unsubscribe'
1142		PresencePacket packet = new PresencePacket();
1143		packet.setAttribute("type", "unsubscribe");
1144		packet.setAttribute("to", contact.getJid());
1145		packet.setAttribute("from", contact.getAccount().getJid());
1146		Log.d(LOGTAG, packet.toString());
1147		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1148	}
1149
1150	public void stopPresenceUpdatesTo(Contact contact) {
1151		// Canceling a Subscription type=unsubscribed
1152		PresencePacket packet = new PresencePacket();
1153		packet.setAttribute("type", "unsubscribed");
1154		packet.setAttribute("to", contact.getJid());
1155		packet.setAttribute("from", contact.getAccount().getJid());
1156		Log.d(LOGTAG, packet.toString());
1157		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1158	}
1159
1160	public void sendPresenceUpdatesTo(Contact contact) {
1161		// type='subscribed'
1162		PresencePacket packet = new PresencePacket();
1163		packet.setAttribute("type", "subscribed");
1164		packet.setAttribute("to", contact.getJid());
1165		packet.setAttribute("from", contact.getAccount().getJid());
1166		Log.d(LOGTAG, packet.toString());
1167		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1168	}
1169
1170	public void sendPgpPresence(Account account, String signature) {
1171		PresencePacket packet = new PresencePacket();
1172		packet.setAttribute("from", account.getFullJid());
1173		Element status = new Element("status");
1174		status.setContent("online");
1175		packet.addChild(status);
1176		Element x = new Element("x");
1177		x.setAttribute("xmlns", "jabber:x:signed");
1178		x.setContent(signature);
1179		packet.addChild(x);
1180		account.getXmppConnection().sendPresencePacket(packet);
1181	}
1182
1183	public void generatePgpAnnouncement(Account account)
1184			throws PgpEngine.UserInputRequiredException {
1185		if (account.getStatus() == Account.STATUS_ONLINE) {
1186			String signature = getPgpEngine().generateSignature("online");
1187			account.setKey("pgp_signature", signature);
1188			databaseBackend.updateAccount(account);
1189			sendPgpPresence(account, signature);
1190		}
1191	}
1192
1193	public void updateConversation(Conversation conversation) {
1194		this.databaseBackend.updateConversation(conversation);
1195	}
1196
1197	public Contact findContact(String uuid) {
1198		Contact contact = this.databaseBackend.getContact(uuid);
1199		for (Account account : getAccounts()) {
1200			if (contact.getAccountUuid().equals(account.getUuid())) {
1201				contact.setAccount(account);
1202			}
1203		}
1204		return contact;
1205	}
1206
1207	public void removeOnTLSExceptionReceivedListener() {
1208		this.tlsException = null;
1209	}
1210
1211	//TODO dont let thread sleep but schedule wake up
1212	public void reconnectAccount(final Account account) {
1213		new Thread(new Runnable() {
1214
1215			@Override
1216			public void run() {
1217				if (account.getXmppConnection() != null) {
1218					disconnect(account, true);
1219				}
1220				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1221					if (account.getXmppConnection() == null) {
1222						account.setXmppConnection(createConnection(account));
1223					}
1224					Thread thread = new Thread(account.getXmppConnection());
1225					thread.start();
1226				}
1227			}
1228		}).start();
1229	}
1230}