XmppConnectionService.java

   1package eu.siacs.conversations.services;
   2
   3import java.text.ParseException;
   4import java.text.SimpleDateFormat;
   5import java.util.Collections;
   6import java.util.Comparator;
   7import java.util.Date;
   8import java.util.Hashtable;
   9import java.util.List;
  10import java.util.Locale;
  11import java.util.Random;
  12
  13import org.openintents.openpgp.OpenPgpError;
  14import org.openintents.openpgp.util.OpenPgpApi;
  15import org.openintents.openpgp.util.OpenPgpServiceConnection;
  16
  17import net.java.otr4j.OtrException;
  18import net.java.otr4j.session.Session;
  19import net.java.otr4j.session.SessionStatus;
  20import eu.siacs.conversations.R;
  21import eu.siacs.conversations.crypto.PgpEngine;
  22import eu.siacs.conversations.entities.Account;
  23import eu.siacs.conversations.entities.Contact;
  24import eu.siacs.conversations.entities.Conversation;
  25import eu.siacs.conversations.entities.Message;
  26import eu.siacs.conversations.entities.MucOptions;
  27import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
  28import eu.siacs.conversations.entities.Presences;
  29import eu.siacs.conversations.parser.MessageParser;
  30import eu.siacs.conversations.persistance.DatabaseBackend;
  31import eu.siacs.conversations.persistance.FileBackend;
  32import eu.siacs.conversations.persistance.OnPhoneContactsMerged;
  33import eu.siacs.conversations.ui.OnAccountListChangedListener;
  34import eu.siacs.conversations.ui.OnConversationListChangedListener;
  35import eu.siacs.conversations.ui.OnRosterFetchedListener;
  36import eu.siacs.conversations.ui.UiCallback;
  37import eu.siacs.conversations.utils.ExceptionHelper;
  38import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
  39import eu.siacs.conversations.utils.PhoneHelper;
  40import eu.siacs.conversations.utils.UIHelper;
  41import eu.siacs.conversations.xml.Element;
  42import eu.siacs.conversations.xmpp.OnBindListener;
  43import eu.siacs.conversations.xmpp.OnIqPacketReceived;
  44import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
  45import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
  46import eu.siacs.conversations.xmpp.OnStatusChanged;
  47import eu.siacs.conversations.xmpp.OnTLSExceptionReceived;
  48import eu.siacs.conversations.xmpp.XmppConnection;
  49import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
  50import eu.siacs.conversations.xmpp.jingle.JingleFile;
  51import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  52import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  53import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  54import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  55import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  56import android.app.AlarmManager;
  57import android.app.PendingIntent;
  58import android.app.Service;
  59import android.content.Context;
  60import android.content.Intent;
  61import android.content.SharedPreferences;
  62import android.database.ContentObserver;
  63import android.database.DatabaseUtils;
  64import android.net.ConnectivityManager;
  65import android.net.NetworkInfo;
  66import android.net.Uri;
  67import android.os.Binder;
  68import android.os.Bundle;
  69import android.os.IBinder;
  70import android.os.PowerManager;
  71import android.os.PowerManager.WakeLock;
  72import android.os.SystemClock;
  73import android.preference.PreferenceManager;
  74import android.provider.ContactsContract;
  75import android.util.Log;
  76
  77public class XmppConnectionService extends Service {
  78
  79	protected static final String LOGTAG = "xmppService";
  80	public DatabaseBackend databaseBackend;
  81	private FileBackend fileBackend;
  82
  83	public long startDate;
  84
  85	private static final int PING_MAX_INTERVAL = 300;
  86	private static final int PING_MIN_INTERVAL = 10;
  87	private static final int PING_TIMEOUT = 5;
  88	private static final int CONNECT_TIMEOUT = 60;
  89	private static final long CARBON_GRACE_PERIOD = 60000L;
  90
  91	private MessageParser mMessageParser = new MessageParser(this);
  92
  93	private List<Account> accounts;
  94	private List<Conversation> conversations = null;
  95	private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
  96			this);
  97
  98	private OnConversationListChangedListener convChangedListener = null;
  99	private int convChangedListenerCount = 0;
 100	private OnAccountListChangedListener accountChangedListener = null;
 101	private OnTLSExceptionReceived tlsException = null;
 102
 103	public void setOnTLSExceptionReceivedListener(
 104			OnTLSExceptionReceived listener) {
 105		tlsException = listener;
 106	}
 107
 108	private Random mRandom = new Random(System.currentTimeMillis());
 109
 110	private long lastCarbonMessageReceived = -CARBON_GRACE_PERIOD;
 111
 112	private ContentObserver contactObserver = new ContentObserver(null) {
 113		@Override
 114		public void onChange(boolean selfChange) {
 115			super.onChange(selfChange);
 116			Log.d(LOGTAG, "contact list has changed");
 117			mergePhoneContactsWithRoster(null);
 118		}
 119	};
 120
 121	private final IBinder mBinder = new XmppConnectionBinder();
 122	private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() {
 123
 124		@Override
 125		public void onMessagePacketReceived(Account account,
 126				MessagePacket packet) {
 127			Message message = null;
 128			boolean notify = true;
 129			if (getPreferences().getBoolean(
 130					"notification_grace_period_after_carbon_received", true)) {
 131				notify = (SystemClock.elapsedRealtime() - lastCarbonMessageReceived) > CARBON_GRACE_PERIOD;
 132			}
 133
 134			if ((packet.getType() == MessagePacket.TYPE_CHAT)) {
 135				String pgpBody = mMessageParser.getPgpBody(packet);
 136				if (pgpBody != null) {
 137					message = mMessageParser.parsePgpChat(pgpBody, packet,
 138							account);
 139					message.markUnread();
 140				} else if ((packet.getBody() != null)
 141						&& (packet.getBody().startsWith("?OTR"))) {
 142					message = mMessageParser.parseOtrChat(packet, account);
 143					if (message != null) {
 144						message.markUnread();
 145					}
 146				} else if (packet.hasChild("body")) {
 147					message = mMessageParser
 148							.parsePlainTextChat(packet, account);
 149					message.markUnread();
 150				} else if (packet.hasChild("received")
 151						|| (packet.hasChild("sent"))) {
 152					message = mMessageParser
 153							.parseCarbonMessage(packet, account);
 154					if (message != null) {
 155						if (message.getStatus() == Message.STATUS_SEND) {
 156							lastCarbonMessageReceived = SystemClock
 157									.elapsedRealtime();
 158							notify = false;
 159							message.getConversation().markRead();
 160						} else {
 161							message.markUnread();
 162						}
 163					}
 164				}
 165
 166			} else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) {
 167				message = mMessageParser.parseGroupchat(packet, account);
 168				if (message != null) {
 169					if (message.getStatus() == Message.STATUS_RECIEVED) {
 170						message.markUnread();
 171					} else {
 172						message.getConversation().markRead();
 173						notify = false;
 174					}
 175				}
 176			} else if (packet.getType() == MessagePacket.TYPE_ERROR) {
 177				mMessageParser.parseError(packet, account);
 178				return;
 179			} else if (packet.getType() == MessagePacket.TYPE_NORMAL) {
 180				if (packet.hasChild("x")) {
 181					Element x = packet.findChild("x");
 182					if (x.hasChild("invite")) {
 183						findOrCreateConversation(account, packet.getFrom(),
 184								true);
 185						if (convChangedListener != null) {
 186							convChangedListener.onConversationListChanged();
 187						}
 188						Log.d(LOGTAG,
 189								"invitation received to " + packet.getFrom());
 190					}
 191
 192				} else {
 193					// Log.d(LOGTAG, "unparsed message " + packet.toString());
 194				}
 195			}
 196			if ((message == null) || (message.getBody() == null)) {
 197				return;
 198			}
 199			if (packet.hasChild("delay")) {
 200				try {
 201					String stamp = packet.findChild("delay").getAttribute(
 202							"stamp");
 203					stamp = stamp.replace("Z", "+0000");
 204					Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
 205							.parse(stamp);
 206					message.setTime(date.getTime());
 207				} catch (ParseException e) {
 208					Log.d(LOGTAG, "error trying to parse date" + e.getMessage());
 209				}
 210			}
 211			Conversation conversation = message.getConversation();
 212			conversation.getMessages().add(message);
 213			if (packet.getType() != MessagePacket.TYPE_ERROR) {
 214				databaseBackend.createMessage(message);
 215			}
 216			if (convChangedListener != null) {
 217				convChangedListener.onConversationListChanged();
 218			} else {
 219				UIHelper.updateNotification(getApplicationContext(),
 220						getConversations(), message.getConversation(), notify);
 221			}
 222		}
 223	};
 224	private OnStatusChanged statusListener = new OnStatusChanged() {
 225
 226		@Override
 227		public void onStatusChanged(Account account) {
 228			if (accountChangedListener != null) {
 229				accountChangedListener.onAccountListChangedListener();
 230			}
 231			if (account.getStatus() == Account.STATUS_ONLINE) {
 232				List<Conversation> conversations = getConversations();
 233				for (int i = 0; i < conversations.size(); ++i) {
 234					if (conversations.get(i).getAccount() == account) {
 235						sendUnsendMessages(conversations.get(i));
 236					}
 237				}
 238				scheduleWakeupCall(PING_MAX_INTERVAL, true);
 239			} else if (account.getStatus() == Account.STATUS_OFFLINE) {
 240				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 241					int timeToReconnect = mRandom.nextInt(50) + 10;
 242					scheduleWakeupCall(timeToReconnect, false);
 243				}
 244
 245			} else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) {
 246				databaseBackend.updateAccount(account);
 247				reconnectAccount(account, true);
 248			} else {
 249				UIHelper.showErrorNotification(getApplicationContext(),
 250						getAccounts());
 251			}
 252		}
 253	};
 254
 255	private OnPresencePacketReceived presenceListener = new OnPresencePacketReceived() {
 256
 257		@Override
 258		public void onPresencePacketReceived(final Account account,
 259				PresencePacket packet) {
 260			if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
 261				Conversation muc = findMuc(
 262						packet.getAttribute("from").split("/")[0], account);
 263				if (muc != null) {
 264					muc.getMucOptions().processPacket(packet);
 265				} else {
 266					Log.d(LOGTAG, account.getJid()
 267							+ ": could not find muc for received muc package "
 268							+ packet.toString());
 269				}
 270			} else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
 271				Conversation muc = findMuc(
 272						packet.getAttribute("from").split("/")[0], account);
 273				if (muc != null) {
 274					Log.d(LOGTAG,
 275							account.getJid() + ": reading muc status packet "
 276									+ packet.toString());
 277					int error = muc.getMucOptions().getError();
 278					muc.getMucOptions().processPacket(packet);
 279					if ((muc.getMucOptions().getError() != error)
 280							&& (convChangedListener != null)) {
 281						Log.d(LOGTAG, "muc error status changed");
 282						convChangedListener.onConversationListChanged();
 283					}
 284				}
 285			} else {
 286				String[] fromParts = packet.getAttribute("from").split("/");
 287				String type = packet.getAttribute("type");
 288				if (fromParts[0].equals(account.getJid())) {
 289					if (fromParts.length == 2) {
 290						if (type == null) {
 291							account.updatePresence(fromParts[1], Presences
 292									.parseShow(packet.findChild("show")));
 293						} else if (type.equals("unavailable")) {
 294							account.removePresence(fromParts[1]);
 295						}
 296					}
 297
 298				} else {
 299					Contact contact = findContact(account, fromParts[0]);
 300					if (contact == null) {
 301						if ("subscribe".equals(type)) {
 302							account.getXmppConnection().addPendingSubscription(
 303									fromParts[0]);
 304						} else {
 305							// Log.d(LOGTAG,packet.getFrom()+
 306							// " could not be found");
 307						}
 308						return;
 309					}
 310					if (type == null) {
 311						if (fromParts.length == 2) {
 312							contact.updatePresence(fromParts[1], Presences
 313									.parseShow(packet.findChild("show")));
 314							PgpEngine pgp = getPgpEngine();
 315							if (pgp != null) {
 316								Element x = packet.findChild("x",
 317										"jabber:x:signed");
 318								if (x != null) {
 319									Element status = packet.findChild("status");
 320									String msg;
 321									if (status != null) {
 322										msg = status.getContent();
 323									} else {
 324										msg = "";
 325									}
 326									contact.setPgpKeyId(pgp.fetchKeyId(account,
 327											msg, x.getContent()));
 328									Log.d("xmppService",
 329											account.getJid()
 330													+ ": fetched key id for "
 331													+ contact.getJid()
 332													+ " was:"
 333													+ contact.getPgpKeyId());
 334								}
 335							}
 336							replaceContactInConversation(account,
 337									contact.getJid(), contact);
 338							databaseBackend.updateContact(contact, true);
 339						} else {
 340							// Log.d(LOGTAG,"presence without resource "+packet.toString());
 341						}
 342					} else if (type.equals("unavailable")) {
 343						if (fromParts.length != 2) {
 344							contact.clearPresences();
 345						} else {
 346							contact.removePresence(fromParts[1]);
 347						}
 348						replaceContactInConversation(account, contact.getJid(),
 349								contact);
 350						databaseBackend.updateContact(contact, true);
 351					} else if (type.equals("subscribe")) {
 352						Log.d(LOGTAG, "received subscribe packet from "
 353								+ packet.getFrom());
 354						if (contact
 355								.getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) {
 356							Log.d(LOGTAG, "preemptive grant; granting");
 357							sendPresenceUpdatesTo(contact);
 358							contact.setSubscriptionOption(Contact.Subscription.FROM);
 359							contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT);
 360							replaceContactInConversation(account,
 361									contact.getJid(), contact);
 362							databaseBackend.updateContact(contact, false);
 363							if ((contact
 364									.getSubscriptionOption(Contact.Subscription.ASKING))
 365									&& (!contact
 366											.getSubscriptionOption(Contact.Subscription.TO))) {
 367								requestPresenceUpdatesFrom(contact);
 368							}
 369						} else {
 370							account.getXmppConnection().addPendingSubscription(
 371									fromParts[0]);
 372						}
 373					} else {
 374						// Log.d(LOGTAG, packet.toString());
 375					}
 376				}
 377			}
 378		}
 379	};
 380
 381	private OnIqPacketReceived unknownIqListener = new OnIqPacketReceived() {
 382
 383		@Override
 384		public void onIqPacketReceived(Account account, IqPacket packet) {
 385			if (packet.hasChild("query", "jabber:iq:roster")) {
 386				String from = packet.getFrom();
 387				if ((from == null) || (from.equals(account.getJid()))) {
 388					Element query = packet.findChild("query");
 389					processRosterItems(account, query);
 390					mergePhoneContactsWithRoster(null);
 391				} else {
 392					Log.d(LOGTAG, "unauthorized roster push from: " + from);
 393				}
 394			} else if (packet
 395					.hasChild("open", "http://jabber.org/protocol/ibb")
 396					|| packet
 397							.hasChild("data", "http://jabber.org/protocol/ibb")) {
 398				XmppConnectionService.this.mJingleConnectionManager
 399						.deliverIbbPacket(account, packet);
 400			} else if (packet.hasChild("query",
 401					"http://jabber.org/protocol/disco#info")) {
 402				IqPacket iqResponse = packet
 403						.generateRespone(IqPacket.TYPE_RESULT);
 404				Element query = iqResponse.addChild("query",
 405						"http://jabber.org/protocol/disco#info");
 406				query.addChild("feature").setAttribute("var",
 407						"urn:xmpp:jingle:1");
 408				query.addChild("feature").setAttribute("var",
 409						"urn:xmpp:jingle:apps:file-transfer:3");
 410				query.addChild("feature").setAttribute("var",
 411						"urn:xmpp:jingle:transports:s5b:1");
 412				query.addChild("feature").setAttribute("var",
 413						"urn:xmpp:jingle:transports:ibb:1");
 414				account.getXmppConnection().sendIqPacket(iqResponse, null);
 415			} else {
 416				if ((packet.getType() == IqPacket.TYPE_GET)
 417						|| (packet.getType() == IqPacket.TYPE_SET)) {
 418					IqPacket response = packet
 419							.generateRespone(IqPacket.TYPE_ERROR);
 420					Element error = response.addChild("error");
 421					error.setAttribute("type", "cancel");
 422					error.addChild("feature-not-implemented",
 423							"urn:ietf:params:xml:ns:xmpp-stanzas");
 424					account.getXmppConnection().sendIqPacket(response, null);
 425				}
 426			}
 427		}
 428	};
 429
 430	private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
 431
 432		@Override
 433		public void onJinglePacketReceived(Account account, JinglePacket packet) {
 434			mJingleConnectionManager.deliverPacket(account, packet);
 435		}
 436	};
 437
 438	private OpenPgpServiceConnection pgpServiceConnection;
 439	private PgpEngine mPgpEngine = null;
 440	private Intent pingIntent;
 441	private PendingIntent pendingPingIntent = null;
 442	private WakeLock wakeLock;
 443	private PowerManager pm;
 444
 445	public PgpEngine getPgpEngine() {
 446		if (pgpServiceConnection.isBound()) {
 447			if (this.mPgpEngine == null) {
 448				this.mPgpEngine = new PgpEngine(new OpenPgpApi(
 449						getApplicationContext(),
 450						pgpServiceConnection.getService()), this);
 451			}
 452			return mPgpEngine;
 453		} else {
 454			return null;
 455		}
 456
 457	}
 458
 459	public FileBackend getFileBackend() {
 460		return this.fileBackend;
 461	}
 462
 463	public Message attachImageToConversation(final Conversation conversation,
 464			final Uri uri, final UiCallback callback) {
 465		final Message message;
 466		if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 467			message = new Message(conversation, "",
 468					Message.ENCRYPTION_DECRYPTED);
 469		} else {
 470			message = new Message(conversation, "", Message.ENCRYPTION_NONE);
 471		}
 472		message.setPresence(conversation.getNextPresence());
 473		message.setType(Message.TYPE_IMAGE);
 474		message.setStatus(Message.STATUS_OFFERED);
 475		new Thread(new Runnable() {
 476
 477			@Override
 478			public void run() {
 479				JingleFile file = getFileBackend().copyImageToPrivateStorage(
 480						message, uri);
 481				if (file == null) {
 482					callback.error(R.string.error_copying_image_file);
 483				} else {
 484					if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 485						getPgpEngine().encrypt(message, callback);
 486					} else {
 487						callback.success();
 488					}
 489				}
 490			}
 491		}).start();
 492		return message;
 493	}
 494
 495	protected Conversation findMuc(String name, Account account) {
 496		for (Conversation conversation : this.conversations) {
 497			if (conversation.getContactJid().split("/")[0].equals(name)
 498					&& (conversation.getAccount() == account)) {
 499				return conversation;
 500			}
 501		}
 502		return null;
 503	}
 504
 505	private void processRosterItems(Account account, Element elements) {
 506		String version = elements.getAttribute("ver");
 507		if (version != null) {
 508			account.setRosterVersion(version);
 509			databaseBackend.updateAccount(account);
 510		}
 511		for (Element item : elements.getChildren()) {
 512			if (item.getName().equals("item")) {
 513				String jid = item.getAttribute("jid");
 514				String subscription = item.getAttribute("subscription");
 515				Contact contact = databaseBackend.findContact(account, jid);
 516				if (contact == null) {
 517					if (!subscription.equals("remove")) {
 518						String name = item.getAttribute("name");
 519						if (name == null) {
 520							name = jid.split("@")[0];
 521						}
 522						contact = new Contact(account, name, jid, null);
 523						contact.parseSubscriptionFromElement(item);
 524						databaseBackend.createContact(contact);
 525					}
 526				} else {
 527					if (subscription.equals("remove")) {
 528						databaseBackend.deleteContact(contact);
 529						replaceContactInConversation(account, contact.getJid(),
 530								null);
 531					} else {
 532						contact.parseSubscriptionFromElement(item);
 533						databaseBackend.updateContact(contact, false);
 534						replaceContactInConversation(account, contact.getJid(),
 535								contact);
 536					}
 537				}
 538			}
 539		}
 540	}
 541
 542	private void replaceContactInConversation(Account account, String jid,
 543			Contact contact) {
 544		List<Conversation> conversations = getConversations();
 545		for (Conversation c : conversations) {
 546			if (c.getContactJid().equals(jid) && (c.getAccount() == account)) {
 547				c.setContact(contact);
 548				break;
 549			}
 550		}
 551	}
 552
 553	public class XmppConnectionBinder extends Binder {
 554		public XmppConnectionService getService() {
 555			return XmppConnectionService.this;
 556		}
 557	}
 558
 559	@Override
 560	public int onStartCommand(Intent intent, int flags, int startId) {
 561		this.wakeLock.acquire();
 562		// Log.d(LOGTAG,"calling start service. caller was:"+intent.getAction());
 563		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
 564				.getSystemService(Context.CONNECTIVITY_SERVICE);
 565		NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
 566		boolean isConnected = activeNetwork != null
 567				&& activeNetwork.isConnected();
 568
 569		for (Account account : accounts) {
 570			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 571				if (!isConnected) {
 572					account.setStatus(Account.STATUS_NO_INTERNET);
 573					if (statusListener != null) {
 574						statusListener.onStatusChanged(account);
 575					}
 576				} else {
 577					if (account.getStatus() == Account.STATUS_NO_INTERNET) {
 578						account.setStatus(Account.STATUS_OFFLINE);
 579						if (statusListener != null) {
 580							statusListener.onStatusChanged(account);
 581						}
 582					}
 583
 584					// TODO 3 remaining cases
 585					if (account.getStatus() == Account.STATUS_ONLINE) {
 586						long lastReceived = account.getXmppConnection().lastPaketReceived;
 587						long lastSent = account.getXmppConnection().lastPingSent;
 588						if (lastSent - lastReceived >= PING_TIMEOUT * 1000) {
 589							Log.d(LOGTAG, account.getJid() + ": ping timeout");
 590							this.reconnectAccount(account, true);
 591						} else if (SystemClock.elapsedRealtime() - lastReceived >= PING_MIN_INTERVAL * 1000) {
 592							account.getXmppConnection().sendPing();
 593							account.getXmppConnection().lastPingSent = SystemClock
 594									.elapsedRealtime();
 595							this.scheduleWakeupCall(2, false);
 596						}
 597					} else if (account.getStatus() == Account.STATUS_OFFLINE) {
 598						if (account.getXmppConnection() == null) {
 599							account.setXmppConnection(this
 600									.createConnection(account));
 601						}
 602						account.getXmppConnection().lastPingSent = SystemClock
 603								.elapsedRealtime();
 604						new Thread(account.getXmppConnection()).start();
 605					} else if ((account.getStatus() == Account.STATUS_CONNECTING)
 606							&& ((SystemClock.elapsedRealtime() - account
 607									.getXmppConnection().lastConnect) / 1000 >= CONNECT_TIMEOUT)) {
 608						Log.d(LOGTAG, account.getJid()
 609								+ ": time out during connect reconnecting");
 610						reconnectAccount(account, true);
 611					} else {
 612						Log.d(LOGTAG,
 613								"seconds since last connect:"
 614										+ ((SystemClock.elapsedRealtime() - account
 615												.getXmppConnection().lastConnect) / 1000));
 616						Log.d(LOGTAG,
 617								account.getJid() + ": status="
 618										+ account.getStatus());
 619						// TODO notify user of ssl cert problem or auth problem
 620						// or what ever
 621					}
 622					// in any case. reschedule wakup call
 623					this.scheduleWakeupCall(PING_MAX_INTERVAL, true);
 624				}
 625				if (accountChangedListener != null) {
 626					accountChangedListener.onAccountListChangedListener();
 627				}
 628			}
 629		}
 630		if (wakeLock.isHeld()) {
 631			wakeLock.release();
 632		}
 633		return START_STICKY;
 634	}
 635
 636	@Override
 637	public void onCreate() {
 638		ExceptionHelper.init(getApplicationContext());
 639		this.databaseBackend = DatabaseBackend
 640				.getInstance(getApplicationContext());
 641		this.fileBackend = new FileBackend(getApplicationContext());
 642		this.accounts = databaseBackend.getAccounts();
 643
 644		this.getConversations();
 645
 646		getContentResolver().registerContentObserver(
 647				ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
 648		this.pgpServiceConnection = new OpenPgpServiceConnection(
 649				getApplicationContext(), "org.sufficientlysecure.keychain");
 650		this.pgpServiceConnection.bindToService();
 651
 652		this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 653		this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
 654				"XmppConnectionService");
 655	}
 656
 657	@Override
 658	public void onDestroy() {
 659		Log.d(LOGTAG, "stopping service");
 660		super.onDestroy();
 661		for (Account account : accounts) {
 662			if (account.getXmppConnection() != null) {
 663				disconnect(account, true);
 664			}
 665		}
 666	}
 667
 668	protected void scheduleWakeupCall(int seconds, boolean ping) {
 669		long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
 670		Context context = getApplicationContext();
 671		AlarmManager alarmManager = (AlarmManager) context
 672				.getSystemService(Context.ALARM_SERVICE);
 673
 674		if (ping) {
 675			if (this.pingIntent == null) {
 676				this.pingIntent = new Intent(context, EventReceiver.class);
 677				this.pingIntent.setAction("ping");
 678				this.pingIntent.putExtra("time", timeToWake);
 679				this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
 680						this.pingIntent, 0);
 681				alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 682						timeToWake, pendingPingIntent);
 683				// Log.d(LOGTAG,"schedule ping in "+seconds+" seconds");
 684			} else {
 685				long scheduledTime = this.pingIntent.getLongExtra("time", 0);
 686				if (scheduledTime < SystemClock.elapsedRealtime()
 687						|| (scheduledTime > timeToWake)) {
 688					this.pingIntent.putExtra("time", timeToWake);
 689					alarmManager.cancel(this.pendingPingIntent);
 690					this.pendingPingIntent = PendingIntent.getBroadcast(
 691							context, 0, this.pingIntent, 0);
 692					alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 693							timeToWake, pendingPingIntent);
 694					// Log.d(LOGTAG,"reschedule old ping to ping in "+seconds+" seconds");
 695				}
 696			}
 697		} else {
 698			Intent intent = new Intent(context, EventReceiver.class);
 699			intent.setAction("ping_check");
 700			PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
 701					intent, 0);
 702			alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake,
 703					alarmIntent);
 704		}
 705
 706	}
 707
 708	public XmppConnection createConnection(Account account) {
 709		SharedPreferences sharedPref = getPreferences();
 710		account.setResource(sharedPref.getString("resource", "mobile")
 711				.toLowerCase(Locale.getDefault()));
 712		XmppConnection connection = new XmppConnection(account, this.pm);
 713		connection.setOnMessagePacketReceivedListener(this.messageListener);
 714		connection.setOnStatusChangedListener(this.statusListener);
 715		connection.setOnPresencePacketReceivedListener(this.presenceListener);
 716		connection
 717				.setOnUnregisteredIqPacketReceivedListener(this.unknownIqListener);
 718		connection.setOnJinglePacketReceivedListener(this.jingleListener);
 719		connection
 720				.setOnTLSExceptionReceivedListener(new OnTLSExceptionReceived() {
 721
 722					@Override
 723					public void onTLSExceptionReceived(String fingerprint,
 724							Account account) {
 725						Log.d(LOGTAG, "tls exception arrived in service");
 726						if (tlsException != null) {
 727							tlsException.onTLSExceptionReceived(fingerprint,
 728									account);
 729						}
 730					}
 731				});
 732		connection.setOnBindListener(new OnBindListener() {
 733
 734			@Override
 735			public void onBind(Account account) {
 736				databaseBackend.clearPresences(account);
 737				account.clearPresences(); // self presences
 738				if (account.getXmppConnection().hasFeatureRosterManagment()) {
 739					updateRoster(account, null);
 740				}
 741				connectMultiModeConversations(account);
 742				if (convChangedListener != null) {
 743					convChangedListener.onConversationListChanged();
 744				}
 745			}
 746		});
 747		return connection;
 748	}
 749
 750	synchronized public void sendMessage(Message message, String presence) {
 751		Account account = message.getConversation().getAccount();
 752		Conversation conv = message.getConversation();
 753		MessagePacket packet = null;
 754		boolean saveInDb = false;
 755		boolean addToConversation = false;
 756		boolean send = false;
 757		if (account.getStatus() == Account.STATUS_ONLINE) {
 758			if (message.getType() == Message.TYPE_IMAGE) {
 759				mJingleConnectionManager.createNewConnection(message);
 760			} else {
 761				if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 762					if (!conv.hasValidOtrSession()) {
 763						// starting otr session. messages will be send later
 764						conv.startOtrSession(getApplicationContext(), presence,
 765								true);
 766					} else if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 767						// otr session aleary exists, creating message packet
 768						// accordingly
 769						packet = prepareMessagePacket(account, message,
 770								conv.getOtrSession());
 771						send = true;
 772						message.setStatus(Message.STATUS_SEND);
 773					}
 774					saveInDb = true;
 775					addToConversation = true;
 776				} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 777					message.getConversation().endOtrIfNeeded();
 778					packet = new MessagePacket();
 779					packet.setType(MessagePacket.TYPE_CHAT);
 780					packet.setFrom(message.getConversation().getAccount()
 781							.getFullJid());
 782					packet.setTo(message.getCounterpart());
 783					packet.setBody("This is an XEP-0027 encryted message");
 784					packet.addChild("x", "jabber:x:encrypted").setContent(
 785							message.getEncryptedBody());
 786					message.setStatus(Message.STATUS_SEND);
 787					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 788					saveInDb = true;
 789					addToConversation = true;
 790					send = true;
 791				} else {
 792					message.getConversation().endOtrIfNeeded();
 793					// don't encrypt
 794					if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
 795						message.setStatus(Message.STATUS_SEND);
 796						saveInDb = true;
 797						addToConversation = true;
 798					}
 799					packet = prepareMessagePacket(account, message, null);
 800					send = true;
 801				}
 802			}
 803		} else {
 804			// account is offline
 805			saveInDb = true;
 806			addToConversation = true;
 807
 808		}
 809		if (saveInDb) {
 810			databaseBackend.createMessage(message);
 811		}
 812		if (addToConversation) {
 813			conv.getMessages().add(message);
 814			if (convChangedListener != null) {
 815				convChangedListener.onConversationListChanged();
 816			}
 817		}
 818		if ((send) && (packet != null)) {
 819			account.getXmppConnection().sendMessagePacket(packet);
 820		}
 821
 822	}
 823
 824	private void sendUnsendMessages(Conversation conversation) {
 825		for (int i = 0; i < conversation.getMessages().size(); ++i) {
 826			if ((conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND)
 827					&& (conversation.getMessages().get(i).getEncryption() == Message.ENCRYPTION_NONE)) {
 828				Message message = conversation.getMessages().get(i);
 829				MessagePacket packet = prepareMessagePacket(
 830						conversation.getAccount(), message, null);
 831				conversation.getAccount().getXmppConnection()
 832						.sendMessagePacket(packet);
 833				message.setStatus(Message.STATUS_SEND);
 834				if (conversation.getMode() == Conversation.MODE_SINGLE) {
 835					databaseBackend.updateMessage(message);
 836				} else {
 837					databaseBackend.deleteMessage(message);
 838					conversation.getMessages().remove(i);
 839					i--;
 840				}
 841			}
 842		}
 843	}
 844
 845	public MessagePacket prepareMessagePacket(Account account, Message message,
 846			Session otrSession) {
 847		MessagePacket packet = new MessagePacket();
 848		if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
 849			packet.setType(MessagePacket.TYPE_CHAT);
 850			if (otrSession != null) {
 851				try {
 852					packet.setBody(otrSession.transformSending(message
 853							.getBody()));
 854				} catch (OtrException e) {
 855					Log.d(LOGTAG,
 856							account.getJid()
 857									+ ": could not encrypt message to "
 858									+ message.getCounterpart());
 859				}
 860				packet.addChild("private", "urn:xmpp:carbons:2");
 861				packet.addChild("no-copy", "urn:xmpp:hints");
 862				packet.setTo(otrSession.getSessionID().getAccountID() + "/"
 863						+ otrSession.getSessionID().getUserID());
 864				packet.setFrom(account.getFullJid());
 865			} else {
 866				packet.setBody(message.getBody());
 867				packet.setTo(message.getCounterpart());
 868				packet.setFrom(account.getJid());
 869			}
 870		} else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 871			packet.setType(MessagePacket.TYPE_GROUPCHAT);
 872			packet.setBody(message.getBody());
 873			packet.setTo(message.getCounterpart().split("/")[0]);
 874			packet.setFrom(account.getJid());
 875		}
 876		packet.setId(message.getUuid());
 877		return packet;
 878	}
 879
 880	private void getRoster(Account account,
 881			final OnRosterFetchedListener listener) {
 882		List<Contact> contacts = databaseBackend.getContactsByAccount(account);
 883		for (int i = 0; i < contacts.size(); ++i) {
 884			contacts.get(i).setAccount(account);
 885		}
 886		if (listener != null) {
 887			listener.onRosterFetched(contacts);
 888		}
 889	}
 890
 891	public List<Contact> getRoster(Account account) {
 892		List<Contact> contacts = databaseBackend.getContactsByAccount(account);
 893		for (int i = 0; i < contacts.size(); ++i) {
 894			contacts.get(i).setAccount(account);
 895		}
 896		return contacts;
 897	}
 898
 899	public void updateRoster(final Account account,
 900			final OnRosterFetchedListener listener) {
 901		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
 902		if (!"".equals(account.getRosterVersion())) {
 903			Log.d(LOGTAG, account.getJid() + ": fetching roster version "
 904					+ account.getRosterVersion());
 905		} else {
 906			Log.d(LOGTAG, account.getJid() + ": fetching roster");
 907		}
 908		iqPacket.query("jabber:iq:roster").setAttribute("ver",
 909				account.getRosterVersion());
 910		account.getXmppConnection().sendIqPacket(iqPacket,
 911				new OnIqPacketReceived() {
 912
 913					@Override
 914					public void onIqPacketReceived(final Account account,
 915							IqPacket packet) {
 916						Element roster = packet.findChild("query");
 917						if (roster != null) {
 918							Log.d(LOGTAG, account.getJid()
 919									+ ": processing roster");
 920							processRosterItems(account, roster);
 921							StringBuilder mWhere = new StringBuilder();
 922							mWhere.append("jid NOT IN(");
 923							List<Element> items = roster.getChildren();
 924							for (int i = 0; i < items.size(); ++i) {
 925								mWhere.append(DatabaseUtils
 926										.sqlEscapeString(items.get(i)
 927												.getAttribute("jid")));
 928								if (i != items.size() - 1) {
 929									mWhere.append(",");
 930								}
 931							}
 932							mWhere.append(") and accountUuid = \"");
 933							mWhere.append(account.getUuid());
 934							mWhere.append("\"");
 935							List<Contact> contactsToDelete = databaseBackend
 936									.getContacts(mWhere.toString());
 937							for (Contact contact : contactsToDelete) {
 938								databaseBackend.deleteContact(contact);
 939								replaceContactInConversation(account,
 940										contact.getJid(), null);
 941							}
 942
 943						} else {
 944							Log.d(LOGTAG, account.getJid()
 945									+ ": empty roster returend");
 946						}
 947						mergePhoneContactsWithRoster(new OnPhoneContactsMerged() {
 948
 949							@Override
 950							public void phoneContactsMerged() {
 951								if (listener != null) {
 952									getRoster(account, listener);
 953								}
 954							}
 955						});
 956					}
 957				});
 958	}
 959
 960	public void mergePhoneContactsWithRoster(
 961			final OnPhoneContactsMerged listener) {
 962		PhoneHelper.loadPhoneContacts(getApplicationContext(),
 963				new OnPhoneContactsLoadedListener() {
 964					@Override
 965					public void onPhoneContactsLoaded(
 966							Hashtable<String, Bundle> phoneContacts) {
 967						List<Contact> contacts = databaseBackend
 968								.getContactsByAccount(null);
 969						for (int i = 0; i < contacts.size(); ++i) {
 970							Contact contact = contacts.get(i);
 971							if (phoneContacts.containsKey(contact.getJid())) {
 972								Bundle phoneContact = phoneContacts.get(contact
 973										.getJid());
 974								String systemAccount = phoneContact
 975										.getInt("phoneid")
 976										+ "#"
 977										+ phoneContact.getString("lookup");
 978								contact.setSystemAccount(systemAccount);
 979								contact.setPhotoUri(phoneContact
 980										.getString("photouri"));
 981								contact.setDisplayName(phoneContact
 982										.getString("displayname"));
 983								databaseBackend.updateContact(contact, false);
 984								replaceContactInConversation(
 985										contact.getAccount(), contact.getJid(),
 986										contact);
 987							} else {
 988								if ((contact.getSystemAccount() != null)
 989										|| (contact.getProfilePhoto() != null)) {
 990									contact.setSystemAccount(null);
 991									contact.setPhotoUri(null);
 992									databaseBackend.updateContact(contact,
 993											false);
 994									replaceContactInConversation(
 995											contact.getAccount(),
 996											contact.getJid(), contact);
 997								}
 998							}
 999						}
1000						if (listener != null) {
1001							listener.phoneContactsMerged();
1002						}
1003					}
1004				});
1005	}
1006
1007	public List<Conversation> getConversations() {
1008		if (this.conversations == null) {
1009			Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
1010			for (Account account : this.accounts) {
1011				accountLookupTable.put(account.getUuid(), account);
1012			}
1013			this.conversations = databaseBackend
1014					.getConversations(Conversation.STATUS_AVAILABLE);
1015			for (Conversation conv : this.conversations) {
1016				Account account = accountLookupTable.get(conv.getAccountUuid());
1017				conv.setAccount(account);
1018				conv.setContact(findContact(account, conv.getContactJid()));
1019				conv.setMessages(databaseBackend.getMessages(conv, 50));
1020			}
1021		}
1022		Collections.sort(this.conversations, new Comparator<Conversation>() {
1023			@Override
1024			public int compare(Conversation lhs, Conversation rhs) {
1025				return (int) (rhs.getLatestMessage().getTimeSent() - lhs
1026						.getLatestMessage().getTimeSent());
1027			}
1028		});
1029		return this.conversations;
1030	}
1031
1032	public List<Account> getAccounts() {
1033		return this.accounts;
1034	}
1035
1036	public Contact findContact(Account account, String jid) {
1037		Contact contact = databaseBackend.findContact(account, jid);
1038		if (contact != null) {
1039			contact.setAccount(account);
1040		}
1041		return contact;
1042	}
1043
1044	public Conversation findOrCreateConversation(Account account, String jid,
1045			boolean muc) {
1046		for (Conversation conv : this.getConversations()) {
1047			if ((conv.getAccount().equals(account))
1048					&& (conv.getContactJid().split("/")[0].equals(jid))) {
1049				return conv;
1050			}
1051		}
1052		Conversation conversation = databaseBackend.findConversation(account,
1053				jid);
1054		if (conversation != null) {
1055			conversation.setStatus(Conversation.STATUS_AVAILABLE);
1056			conversation.setAccount(account);
1057			if (muc) {
1058				conversation.setMode(Conversation.MODE_MULTI);
1059			} else {
1060				conversation.setMode(Conversation.MODE_SINGLE);
1061			}
1062			conversation.setMessages(databaseBackend.getMessages(conversation,
1063					50));
1064			this.databaseBackend.updateConversation(conversation);
1065			conversation.setContact(findContact(account,
1066					conversation.getContactJid()));
1067		} else {
1068			String conversationName;
1069			Contact contact = findContact(account, jid);
1070			if (contact != null) {
1071				conversationName = contact.getDisplayName();
1072			} else {
1073				conversationName = jid.split("@")[0];
1074			}
1075			if (muc) {
1076				conversation = new Conversation(conversationName, account, jid,
1077						Conversation.MODE_MULTI);
1078			} else {
1079				conversation = new Conversation(conversationName, account, jid,
1080						Conversation.MODE_SINGLE);
1081			}
1082			conversation.setContact(contact);
1083			this.databaseBackend.createConversation(conversation);
1084		}
1085		this.conversations.add(conversation);
1086		if ((account.getStatus() == Account.STATUS_ONLINE)
1087				&& (conversation.getMode() == Conversation.MODE_MULTI)) {
1088			joinMuc(conversation);
1089		}
1090		if (this.convChangedListener != null) {
1091			this.convChangedListener.onConversationListChanged();
1092		}
1093		return conversation;
1094	}
1095
1096	public void archiveConversation(Conversation conversation) {
1097		if (conversation.getMode() == Conversation.MODE_MULTI) {
1098			leaveMuc(conversation);
1099		} else {
1100			conversation.endOtrIfNeeded();
1101		}
1102		this.databaseBackend.updateConversation(conversation);
1103		this.conversations.remove(conversation);
1104		if (this.convChangedListener != null) {
1105			this.convChangedListener.onConversationListChanged();
1106		}
1107	}
1108
1109	public void clearConversationHistory(Conversation conversation) {
1110		this.databaseBackend.deleteMessagesInConversation(conversation);
1111		this.fileBackend.removeFiles(conversation);
1112		conversation.getMessages().clear();
1113		if (this.convChangedListener != null) {
1114			this.convChangedListener.onConversationListChanged();
1115		}
1116	}
1117
1118	public int getConversationCount() {
1119		return this.databaseBackend.getConversationCount();
1120	}
1121
1122	public void createAccount(Account account) {
1123		databaseBackend.createAccount(account);
1124		this.accounts.add(account);
1125		this.reconnectAccount(account, false);
1126		if (accountChangedListener != null)
1127			accountChangedListener.onAccountListChangedListener();
1128	}
1129
1130	public void deleteContact(Contact contact) {
1131		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1132		Element query = iq.query("jabber:iq:roster");
1133		query.addChild("item").setAttribute("jid", contact.getJid())
1134				.setAttribute("subscription", "remove");
1135		contact.getAccount().getXmppConnection().sendIqPacket(iq, null);
1136		replaceContactInConversation(contact.getAccount(), contact.getJid(),
1137				null);
1138		databaseBackend.deleteContact(contact);
1139	}
1140
1141	public void updateAccount(Account account) {
1142		this.statusListener.onStatusChanged(account);
1143		databaseBackend.updateAccount(account);
1144		reconnectAccount(account, false);
1145		if (accountChangedListener != null)
1146			accountChangedListener.onAccountListChangedListener();
1147	}
1148
1149	public void deleteAccount(Account account) {
1150		if (account.getXmppConnection() != null) {
1151			this.disconnect(account, true);
1152		}
1153		databaseBackend.deleteAccount(account);
1154		this.accounts.remove(account);
1155		if (accountChangedListener != null)
1156			accountChangedListener.onAccountListChangedListener();
1157	}
1158
1159	public void setOnConversationListChangedListener(
1160			OnConversationListChangedListener listener) {
1161		this.convChangedListener = listener;
1162		this.convChangedListenerCount++;
1163	}
1164
1165	public void removeOnConversationListChangedListener() {
1166		this.convChangedListenerCount--;
1167		if (this.convChangedListenerCount == 0) {
1168			this.convChangedListener = null;
1169		}
1170	}
1171
1172	public void setOnAccountListChangedListener(
1173			OnAccountListChangedListener listener) {
1174		this.accountChangedListener = listener;
1175	}
1176
1177	public void removeOnAccountListChangedListener() {
1178		this.accountChangedListener = null;
1179	}
1180
1181	public void connectMultiModeConversations(Account account) {
1182		List<Conversation> conversations = getConversations();
1183		for (int i = 0; i < conversations.size(); i++) {
1184			Conversation conversation = conversations.get(i);
1185			if ((conversation.getMode() == Conversation.MODE_MULTI)
1186					&& (conversation.getAccount() == account)) {
1187				joinMuc(conversation);
1188			}
1189		}
1190	}
1191
1192	public void joinMuc(Conversation conversation) {
1193		String[] mucParts = conversation.getContactJid().split("/");
1194		String muc;
1195		String nick;
1196		if (mucParts.length == 2) {
1197			muc = mucParts[0];
1198			nick = mucParts[1];
1199		} else {
1200			muc = mucParts[0];
1201			nick = conversation.getAccount().getUsername();
1202		}
1203		PresencePacket packet = new PresencePacket();
1204		packet.setAttribute("to", muc + "/" + nick);
1205		Element x = new Element("x");
1206		x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
1207		if (conversation.getMessages().size() != 0) {
1208			long lastMsgTime = conversation.getLatestMessage().getTimeSent();
1209			long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1;
1210			x.addChild("history").setAttribute("seconds", diff + "");
1211		}
1212		packet.addChild(x);
1213		conversation.getAccount().getXmppConnection()
1214				.sendPresencePacket(packet);
1215	}
1216
1217	private OnRenameListener renameListener = null;
1218
1219	public void setOnRenameListener(OnRenameListener listener) {
1220		this.renameListener = listener;
1221	}
1222
1223	public void renameInMuc(final Conversation conversation, final String nick) {
1224		final MucOptions options = conversation.getMucOptions();
1225		if (options.online()) {
1226			options.setOnRenameListener(new OnRenameListener() {
1227
1228				@Override
1229				public void onRename(boolean success) {
1230					if (renameListener != null) {
1231						renameListener.onRename(success);
1232					}
1233					if (success) {
1234						String jid = conversation.getContactJid().split("/")[0]
1235								+ "/" + nick;
1236						conversation.setContactJid(jid);
1237						databaseBackend.updateConversation(conversation);
1238					}
1239				}
1240			});
1241			options.flagAboutToRename();
1242			PresencePacket packet = new PresencePacket();
1243			packet.setAttribute("to",
1244					conversation.getContactJid().split("/")[0] + "/" + nick);
1245			packet.setAttribute("from", conversation.getAccount().getFullJid());
1246
1247			conversation.getAccount().getXmppConnection()
1248					.sendPresencePacket(packet, null);
1249		} else {
1250			String jid = conversation.getContactJid().split("/")[0] + "/"
1251					+ nick;
1252			conversation.setContactJid(jid);
1253			databaseBackend.updateConversation(conversation);
1254			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1255				joinMuc(conversation);
1256			}
1257		}
1258	}
1259
1260	public void leaveMuc(Conversation conversation) {
1261		PresencePacket packet = new PresencePacket();
1262		packet.setAttribute("to", conversation.getContactJid().split("/")[0]
1263				+ "/" + conversation.getMucOptions().getNick());
1264		packet.setAttribute("from", conversation.getAccount().getFullJid());
1265		packet.setAttribute("type", "unavailable");
1266		Log.d(LOGTAG, "send leaving muc " + packet);
1267		conversation.getAccount().getXmppConnection()
1268				.sendPresencePacket(packet);
1269		conversation.getMucOptions().setOffline();
1270	}
1271
1272	public void disconnect(Account account, boolean force) {
1273		if ((account.getStatus() == Account.STATUS_ONLINE)
1274				|| (account.getStatus() == Account.STATUS_DISABLED)) {
1275			if (!force) {
1276				List<Conversation> conversations = getConversations();
1277				for (int i = 0; i < conversations.size(); i++) {
1278					Conversation conversation = conversations.get(i);
1279					if (conversation.getAccount() == account) {
1280						if (conversation.getMode() == Conversation.MODE_MULTI) {
1281							leaveMuc(conversation);
1282						} else {
1283							conversation.endOtrIfNeeded();
1284						}
1285					}
1286				}
1287			}
1288			account.getXmppConnection().disconnect(force);
1289		}
1290	}
1291
1292	@Override
1293	public IBinder onBind(Intent intent) {
1294		return mBinder;
1295	}
1296
1297	public void updateContact(Contact contact) {
1298		databaseBackend.updateContact(contact, false);
1299		replaceContactInConversation(contact.getAccount(), contact.getJid(),
1300				contact);
1301	}
1302
1303	public void updateMessage(Message message) {
1304		databaseBackend.updateMessage(message);
1305	}
1306
1307	public void createContact(Contact contact) {
1308		SharedPreferences sharedPref = getPreferences();
1309		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1310		if (autoGrant) {
1311			contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT);
1312			contact.setSubscriptionOption(Contact.Subscription.ASKING);
1313		}
1314		databaseBackend.createContact(contact);
1315		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1316		Element query = new Element("query");
1317		query.setAttribute("xmlns", "jabber:iq:roster");
1318		Element item = new Element("item");
1319		item.setAttribute("jid", contact.getJid());
1320		item.setAttribute("name", contact.getJid());
1321		query.addChild(item);
1322		iq.addChild(query);
1323		Account account = contact.getAccount();
1324		account.getXmppConnection().sendIqPacket(iq, null);
1325		if (autoGrant) {
1326			requestPresenceUpdatesFrom(contact);
1327			if (account.getXmppConnection().hasPendingSubscription(
1328					contact.getJid())) {
1329				Log.d("xmppService", "contact had pending subscription");
1330				sendPresenceUpdatesTo(contact);
1331			}
1332		}
1333		replaceContactInConversation(contact.getAccount(), contact.getJid(),
1334				contact);
1335	}
1336
1337	public void requestPresenceUpdatesFrom(Contact contact) {
1338		// Requesting a Subscription type=subscribe
1339		PresencePacket packet = new PresencePacket();
1340		packet.setAttribute("type", "subscribe");
1341		packet.setAttribute("to", contact.getJid());
1342		packet.setAttribute("from", contact.getAccount().getJid());
1343		Log.d(LOGTAG, packet.toString());
1344		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1345	}
1346
1347	public void stopPresenceUpdatesFrom(Contact contact) {
1348		// Unsubscribing type='unsubscribe'
1349		PresencePacket packet = new PresencePacket();
1350		packet.setAttribute("type", "unsubscribe");
1351		packet.setAttribute("to", contact.getJid());
1352		packet.setAttribute("from", contact.getAccount().getJid());
1353		Log.d(LOGTAG, packet.toString());
1354		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1355	}
1356
1357	public void stopPresenceUpdatesTo(Contact contact) {
1358		// Canceling a Subscription type=unsubscribed
1359		PresencePacket packet = new PresencePacket();
1360		packet.setAttribute("type", "unsubscribed");
1361		packet.setAttribute("to", contact.getJid());
1362		packet.setAttribute("from", contact.getAccount().getJid());
1363		Log.d(LOGTAG, packet.toString());
1364		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1365	}
1366
1367	public void sendPresenceUpdatesTo(Contact contact) {
1368		// type='subscribed'
1369		PresencePacket packet = new PresencePacket();
1370		packet.setAttribute("type", "subscribed");
1371		packet.setAttribute("to", contact.getJid());
1372		packet.setAttribute("from", contact.getAccount().getJid());
1373		Log.d(LOGTAG, packet.toString());
1374		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1375	}
1376
1377	public void sendPgpPresence(Account account, String signature) {
1378		PresencePacket packet = new PresencePacket();
1379		packet.setAttribute("from", account.getFullJid());
1380		Element status = new Element("status");
1381		status.setContent("online");
1382		packet.addChild(status);
1383		Element x = new Element("x");
1384		x.setAttribute("xmlns", "jabber:x:signed");
1385		x.setContent(signature);
1386		packet.addChild(x);
1387		account.getXmppConnection().sendPresencePacket(packet);
1388	}
1389
1390	public void updateConversation(Conversation conversation) {
1391		this.databaseBackend.updateConversation(conversation);
1392	}
1393
1394	public Contact findContact(String uuid) {
1395		Contact contact = this.databaseBackend.getContact(uuid);
1396		if (contact != null) {
1397			for (Account account : getAccounts()) {
1398				if (contact.getAccountUuid().equals(account.getUuid())) {
1399					contact.setAccount(account);
1400				}
1401			}
1402		}
1403		return contact;
1404	}
1405
1406	public void removeOnTLSExceptionReceivedListener() {
1407		this.tlsException = null;
1408	}
1409
1410	// TODO dont let thread sleep but schedule wake up
1411	public void reconnectAccount(final Account account, final boolean force) {
1412		new Thread(new Runnable() {
1413
1414			@Override
1415			public void run() {
1416				if (account.getXmppConnection() != null) {
1417					disconnect(account, force);
1418				}
1419				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1420					if (account.getXmppConnection() == null) {
1421						account.setXmppConnection(createConnection(account));
1422					}
1423					Thread thread = new Thread(account.getXmppConnection());
1424					thread.start();
1425					scheduleWakeupCall((int) (CONNECT_TIMEOUT * 1.2), false);
1426				}
1427			}
1428		}).start();
1429	}
1430
1431	public void sendConversationSubject(Conversation conversation,
1432			String subject) {
1433		MessagePacket packet = new MessagePacket();
1434		packet.setType(MessagePacket.TYPE_GROUPCHAT);
1435		packet.setTo(conversation.getContactJid().split("/")[0]);
1436		Element subjectChild = new Element("subject");
1437		subjectChild.setContent(subject);
1438		packet.addChild(subjectChild);
1439		packet.setFrom(conversation.getAccount().getJid());
1440		Account account = conversation.getAccount();
1441		if (account.getStatus() == Account.STATUS_ONLINE) {
1442			account.getXmppConnection().sendMessagePacket(packet);
1443		}
1444	}
1445
1446	public void inviteToConference(Conversation conversation,
1447			List<Contact> contacts) {
1448		for (Contact contact : contacts) {
1449			MessagePacket packet = new MessagePacket();
1450			packet.setTo(conversation.getContactJid().split("/")[0]);
1451			packet.setFrom(conversation.getAccount().getFullJid());
1452			Element x = new Element("x");
1453			x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
1454			Element invite = new Element("invite");
1455			invite.setAttribute("to", contact.getJid());
1456			x.addChild(invite);
1457			packet.addChild(x);
1458			Log.d(LOGTAG, packet.toString());
1459			conversation.getAccount().getXmppConnection()
1460					.sendMessagePacket(packet);
1461		}
1462
1463	}
1464
1465	public boolean markMessage(Account account, String recipient, String uuid,
1466			int status) {
1467		boolean marked = false;
1468		for (Conversation conversation : getConversations()) {
1469			if (conversation.getContactJid().equals(recipient)
1470					&& conversation.getAccount().equals(account)) {
1471				for (Message message : conversation.getMessages()) {
1472					if (message.getUuid().equals(uuid)) {
1473						markMessage(message, status);
1474						marked = true;
1475						break;
1476					}
1477				}
1478				break;
1479			}
1480		}
1481		return marked;
1482	}
1483
1484	public void markMessage(Message message, int status) {
1485		message.setStatus(status);
1486		databaseBackend.updateMessage(message);
1487		if (convChangedListener != null) {
1488			convChangedListener.onConversationListChanged();
1489		}
1490	}
1491
1492	public SharedPreferences getPreferences() {
1493		return PreferenceManager
1494				.getDefaultSharedPreferences(getApplicationContext());
1495	}
1496
1497	public void updateUi(Conversation conversation, boolean notify) {
1498		if (convChangedListener != null) {
1499			convChangedListener.onConversationListChanged();
1500		} else {
1501			UIHelper.updateNotification(getApplicationContext(),
1502					getConversations(), conversation, notify);
1503		}
1504	}
1505}