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