XmppConnectionService.java

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