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