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