XmppConnectionService.java

   1package eu.siacs.conversations.services;
   2
   3import android.annotation.SuppressLint;
   4import android.app.AlarmManager;
   5import android.app.PendingIntent;
   6import android.app.Service;
   7import android.content.Context;
   8import android.content.Intent;
   9import android.content.SharedPreferences;
  10import android.database.ContentObserver;
  11import android.graphics.Bitmap;
  12import android.net.ConnectivityManager;
  13import android.net.NetworkInfo;
  14import android.net.Uri;
  15import android.os.Binder;
  16import android.os.Bundle;
  17import android.os.FileObserver;
  18import android.os.IBinder;
  19import android.os.PowerManager;
  20import android.os.PowerManager.WakeLock;
  21import android.os.SystemClock;
  22import android.preference.PreferenceManager;
  23import android.provider.ContactsContract;
  24import android.util.Log;
  25import android.util.LruCache;
  26
  27import net.java.otr4j.OtrException;
  28import net.java.otr4j.session.Session;
  29import net.java.otr4j.session.SessionID;
  30import net.java.otr4j.session.SessionStatus;
  31
  32import org.openintents.openpgp.util.OpenPgpApi;
  33import org.openintents.openpgp.util.OpenPgpServiceConnection;
  34
  35import java.io.FileNotFoundException;
  36import java.io.IOException;
  37import java.io.OutputStream;
  38import java.security.SecureRandom;
  39import java.text.SimpleDateFormat;
  40import java.util.ArrayList;
  41import java.util.Collections;
  42import java.util.Comparator;
  43import java.util.Date;
  44import java.util.Hashtable;
  45import java.util.List;
  46import java.util.Locale;
  47import java.util.TimeZone;
  48import java.util.concurrent.CopyOnWriteArrayList;
  49
  50import de.duenndns.ssl.MemorizingTrustManager;
  51import eu.siacs.conversations.Config;
  52import eu.siacs.conversations.R;
  53import eu.siacs.conversations.crypto.PgpEngine;
  54import eu.siacs.conversations.entities.Account;
  55import eu.siacs.conversations.entities.Bookmark;
  56import eu.siacs.conversations.entities.Contact;
  57import eu.siacs.conversations.entities.Conversation;
  58import eu.siacs.conversations.entities.Downloadable;
  59import eu.siacs.conversations.entities.DownloadableFile;
  60import eu.siacs.conversations.entities.Message;
  61import eu.siacs.conversations.entities.MucOptions;
  62import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
  63import eu.siacs.conversations.entities.Presences;
  64import eu.siacs.conversations.generator.IqGenerator;
  65import eu.siacs.conversations.generator.MessageGenerator;
  66import eu.siacs.conversations.generator.PresenceGenerator;
  67import eu.siacs.conversations.http.HttpConnectionManager;
  68import eu.siacs.conversations.parser.IqParser;
  69import eu.siacs.conversations.parser.MessageParser;
  70import eu.siacs.conversations.parser.PresenceParser;
  71import eu.siacs.conversations.persistance.DatabaseBackend;
  72import eu.siacs.conversations.persistance.FileBackend;
  73import eu.siacs.conversations.ui.UiCallback;
  74import eu.siacs.conversations.utils.CryptoHelper;
  75import eu.siacs.conversations.utils.ExceptionHelper;
  76import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
  77import eu.siacs.conversations.utils.PRNGFixes;
  78import eu.siacs.conversations.utils.PhoneHelper;
  79import eu.siacs.conversations.utils.UIHelper;
  80import eu.siacs.conversations.xml.Element;
  81import eu.siacs.conversations.xmpp.OnBindListener;
  82import eu.siacs.conversations.xmpp.OnContactStatusChanged;
  83import eu.siacs.conversations.xmpp.OnIqPacketReceived;
  84import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
  85import eu.siacs.conversations.xmpp.OnStatusChanged;
  86import eu.siacs.conversations.xmpp.XmppConnection;
  87import eu.siacs.conversations.xmpp.jid.InvalidJidException;
  88import eu.siacs.conversations.xmpp.jid.Jid;
  89import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
  90import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  91import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  92import eu.siacs.conversations.xmpp.pep.Avatar;
  93import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  94import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  95import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  96
  97public class XmppConnectionService extends Service {
  98
  99	public static String ACTION_CLEAR_NOTIFICATION = "clear_notification";
 100	private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
 101	private ContentObserver contactObserver = new ContentObserver(null) {
 102		@Override
 103		public void onChange(boolean selfChange) {
 104			super.onChange(selfChange);
 105			Intent intent = new Intent(getApplicationContext(),
 106					XmppConnectionService.class);
 107			intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
 108			startService(intent);
 109		}
 110	};
 111	private final IBinder mBinder = new XmppConnectionBinder();
 112	public DatabaseBackend databaseBackend;
 113	public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
 114
 115		@Override
 116		public void onContactStatusChanged(Contact contact, boolean online) {
 117			Conversation conversation = find(getConversations(), contact);
 118			if (conversation != null) {
 119				if (online && contact.getPresences().size() > 1) {
 120					conversation.endOtrIfNeeded();
 121				} else {
 122					conversation.resetOtrSession();
 123				}
 124				if (online && (contact.getPresences().size() == 1)) {
 125					sendUnsendMessages(conversation);
 126				}
 127			}
 128		}
 129	};
 130	private FileBackend fileBackend = new FileBackend(this);
 131	private MemorizingTrustManager mMemorizingTrustManager;
 132	private NotificationService mNotificationService = new NotificationService(
 133			this);
 134	private MessageParser mMessageParser = new MessageParser(this);
 135	private PresenceParser mPresenceParser = new PresenceParser(this);
 136	private IqParser mIqParser = new IqParser(this);
 137	private MessageGenerator mMessageGenerator = new MessageGenerator(this);
 138	private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
 139	private List<Account> accounts;
 140	private CopyOnWriteArrayList<Conversation> conversations = null;
 141	private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
 142			this);
 143	private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
 144			this);
 145	private AvatarService mAvatarService = new AvatarService(this);
 146	private OnConversationUpdate mOnConversationUpdate = null;
 147	private Integer convChangedListenerCount = 0;
 148	private OnAccountUpdate mOnAccountUpdate = null;
 149	private OnStatusChanged statusListener = new OnStatusChanged() {
 150
 151		@Override
 152		public void onStatusChanged(Account account) {
 153			XmppConnection connection = account.getXmppConnection();
 154			if (mOnAccountUpdate != null) {
 155				mOnAccountUpdate.onAccountUpdate();
 156			}
 157			if (account.getStatus() == Account.STATUS_ONLINE) {
 158				for (Conversation conversation : account.pendingConferenceLeaves) {
 159					leaveMuc(conversation);
 160				}
 161				for (Conversation conversation : account.pendingConferenceJoins) {
 162					joinMuc(conversation);
 163				}
 164				mJingleConnectionManager.cancelInTransmission();
 165				List<Conversation> conversations = getConversations();
 166				for (Conversation conversation : conversations) {
 167					if (conversation.getAccount() == account) {
 168						conversation.startOtrIfNeeded();
 169						sendUnsendMessages(conversation);
 170					}
 171				}
 172				if (connection != null && connection.getFeatures().csi()) {
 173					if (checkListeners()) {
 174						Log.d(Config.LOGTAG, account.getJid().toBareJid()
 175								+ " sending csi//inactive");
 176						connection.sendInactive();
 177					} else {
 178						Log.d(Config.LOGTAG, account.getJid().toBareJid()
 179								+ " sending csi//active");
 180						connection.sendActive();
 181					}
 182				}
 183				syncDirtyContacts(account);
 184				scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
 185			} else if (account.getStatus() == Account.STATUS_OFFLINE) {
 186				resetSendingToWaiting(account);
 187				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 188					int timeToReconnect = mRandom.nextInt(50) + 10;
 189					scheduleWakeupCall(timeToReconnect, false);
 190				}
 191			} else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) {
 192				databaseBackend.updateAccount(account);
 193				reconnectAccount(account, true);
 194			} else if ((account.getStatus() != Account.STATUS_CONNECTING)
 195					&& (account.getStatus() != Account.STATUS_NO_INTERNET)) {
 196				if (connection != null) {
 197					int next = connection.getTimeToNextAttempt();
 198					Log.d(Config.LOGTAG, account.getJid().toBareJid()
 199							+ ": error connecting account. try again in "
 200							+ next + "s for the "
 201							+ (connection.getAttempt() + 1) + " time");
 202					scheduleWakeupCall((int) (next * 1.2), false);
 203				}
 204			}
 205			UIHelper.showErrorNotification(getApplicationContext(),
 206					getAccounts());
 207		}
 208	};
 209	private Integer accountChangedListenerCount = 0;
 210	private OnRosterUpdate mOnRosterUpdate = null;
 211	private Integer rosterChangedListenerCount = 0;
 212	private SecureRandom mRandom;
 213	private FileObserver fileObserver = new FileObserver(
 214			FileBackend.getConversationsDirectory()) {
 215
 216		@Override
 217		public void onEvent(int event, String path) {
 218			if (event == FileObserver.DELETE) {
 219				markFileDeleted(path.split("\\.")[0]);
 220			}
 221		}
 222	};
 223	private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
 224
 225		@Override
 226		public void onJinglePacketReceived(Account account, JinglePacket packet) {
 227			mJingleConnectionManager.deliverPacket(account, packet);
 228		}
 229	};
 230
 231	private OpenPgpServiceConnection pgpServiceConnection;
 232	private PgpEngine mPgpEngine = null;
 233	private Intent pingIntent;
 234	private PendingIntent pendingPingIntent = null;
 235	private WakeLock wakeLock;
 236	private PowerManager pm;
 237	private OnBindListener mOnBindListener = new OnBindListener() {
 238
 239		@Override
 240		public void onBind(final Account account) {
 241			account.getRoster().clearPresences();
 242			account.clearPresences(); // self presences
 243			account.pendingConferenceJoins.clear();
 244			account.pendingConferenceLeaves.clear();
 245			fetchRosterFromServer(account);
 246			fetchBookmarks(account);
 247			sendPresencePacket(account,
 248					mPresenceGenerator.sendPresence(account));
 249			connectMultiModeConversations(account);
 250			updateConversationUi();
 251		}
 252	};
 253
 254	private OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
 255
 256		@Override
 257		public void onMessageAcknowledged(Account account, String uuid) {
 258			for (Conversation conversation : getConversations()) {
 259				if (conversation.getAccount() == account) {
 260					for (Message message : conversation.getMessages()) {
 261						if ((message.getStatus() == Message.STATUS_UNSEND || message
 262								.getStatus() == Message.STATUS_WAITING)
 263								&& message.getUuid().equals(uuid)) {
 264							markMessage(message, Message.STATUS_SEND);
 265							return;
 266						}
 267					}
 268				}
 269			}
 270		}
 271	};
 272	private LruCache<String, Bitmap> mBitmapCache;
 273	private OnRenameListener renameListener = null;
 274	private IqGenerator mIqGenerator = new IqGenerator(this);
 275
 276	public PgpEngine getPgpEngine() {
 277		if (pgpServiceConnection.isBound()) {
 278			if (this.mPgpEngine == null) {
 279				this.mPgpEngine = new PgpEngine(new OpenPgpApi(
 280						getApplicationContext(),
 281						pgpServiceConnection.getService()), this);
 282			}
 283			return mPgpEngine;
 284		} else {
 285			return null;
 286		}
 287
 288	}
 289
 290	public FileBackend getFileBackend() {
 291		return this.fileBackend;
 292	}
 293
 294	public AvatarService getAvatarService() {
 295		return this.mAvatarService;
 296	}
 297
 298	public Message attachFileToConversation(Conversation conversation, final Uri uri) {
 299		String path = getFileBackend().getOriginalPath(uri);
 300		if (path!=null) {
 301			Log.d(Config.LOGTAG,"file path : "+path);
 302			Message message;
 303			if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 304				message = new Message(conversation, "",
 305						Message.ENCRYPTION_DECRYPTED);
 306			} else {
 307				message = new Message(conversation, "",
 308						conversation.getNextEncryption(forceEncryption()));
 309			}
 310			message.setCounterpart(conversation.getNextCounterpart());
 311			message.setType(Message.TYPE_FILE);
 312			message.setStatus(Message.STATUS_OFFERED);
 313			message.setRelativeFilePath(path);
 314			getFileBackend().updateFileParams(message);
 315			return message;
 316		}
 317		return null;
 318	}
 319
 320	public Message attachImageToConversation(final Conversation conversation,
 321											 final Uri uri, final UiCallback<Message> callback) {
 322		final Message message;
 323		if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 324			message = new Message(conversation, "",
 325					Message.ENCRYPTION_DECRYPTED);
 326		} else {
 327			message = new Message(conversation, "",
 328					conversation.getNextEncryption(forceEncryption()));
 329		}
 330		message.setCounterpart(conversation.getNextCounterpart());
 331		message.setType(Message.TYPE_IMAGE);
 332		message.setStatus(Message.STATUS_OFFERED);
 333		new Thread(new Runnable() {
 334
 335			@Override
 336			public void run() {
 337				try {
 338					DownloadableFile file = getFileBackend().copyImageToPrivateStorage(message, uri);
 339					if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 340						getPgpEngine().encrypt(message, callback);
 341					} else {
 342						callback.success(message);
 343					}
 344				} catch (FileBackend.FileCopyException e) {
 345					callback.error(e.getResId(), message);
 346				}
 347			}
 348		}).start();
 349		return message;
 350	}
 351
 352	public Conversation find(Bookmark bookmark) {
 353		return find(bookmark.getAccount(), bookmark.getJid());
 354	}
 355
 356	public Conversation find(final Account account, final Jid jid) {
 357		return find(getConversations(), account, jid);
 358	}
 359
 360	@Override
 361	public int onStartCommand(Intent intent, int flags, int startId) {
 362		if (intent != null && intent.getAction() != null) {
 363			if (intent.getAction().equals(ACTION_MERGE_PHONE_CONTACTS)) {
 364				mergePhoneContactsWithRoster();
 365				return START_STICKY;
 366			} else if (intent.getAction().equals(Intent.ACTION_SHUTDOWN)) {
 367				logoutAndSave();
 368				return START_NOT_STICKY;
 369			} else if (intent.getAction().equals(ACTION_CLEAR_NOTIFICATION)) {
 370				mNotificationService.clear();
 371			}
 372		}
 373		this.wakeLock.acquire();
 374
 375		for (Account account : accounts) {
 376			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 377				if (!hasInternetConnection()) {
 378					account.setStatus(Account.STATUS_NO_INTERNET);
 379					if (statusListener != null) {
 380						statusListener.onStatusChanged(account);
 381					}
 382				} else {
 383					if (account.getStatus() == Account.STATUS_NO_INTERNET) {
 384						account.setStatus(Account.STATUS_OFFLINE);
 385						if (statusListener != null) {
 386							statusListener.onStatusChanged(account);
 387						}
 388					}
 389					if (account.getStatus() == Account.STATUS_ONLINE) {
 390						long lastReceived = account.getXmppConnection()
 391								.getLastPacketReceived();
 392						long lastSent = account.getXmppConnection()
 393								.getLastPingSent();
 394						if (lastSent - lastReceived >= Config.PING_TIMEOUT * 1000) {
 395							Log.d(Config.LOGTAG, account.getJid()
 396									+ ": ping timeout");
 397							this.reconnectAccount(account, true);
 398						} else if (SystemClock.elapsedRealtime() - lastReceived >= Config.PING_MIN_INTERVAL * 1000) {
 399							account.getXmppConnection().sendPing();
 400							this.scheduleWakeupCall(2, false);
 401						}
 402					} else if (account.getStatus() == Account.STATUS_OFFLINE) {
 403						if (account.getXmppConnection() == null) {
 404							account.setXmppConnection(this
 405									.createConnection(account));
 406						}
 407						new Thread(account.getXmppConnection()).start();
 408					} else if ((account.getStatus() == Account.STATUS_CONNECTING)
 409							&& ((SystemClock.elapsedRealtime() - account
 410							.getXmppConnection().getLastConnect()) / 1000 >= Config.CONNECT_TIMEOUT)) {
 411						Log.d(Config.LOGTAG, account.getJid()
 412								+ ": time out during connect reconnecting");
 413						reconnectAccount(account, true);
 414					} else {
 415						if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
 416							reconnectAccount(account, true);
 417						}
 418					}
 419					// in any case. reschedule wakup call
 420					this.scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
 421				}
 422				if (mOnAccountUpdate != null) {
 423					mOnAccountUpdate.onAccountUpdate();
 424				}
 425			}
 426		}
 427		/*PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE);
 428		if (!pm.isScreenOn()) {
 429			removeStaleListeners();
 430		}*/
 431		if (wakeLock.isHeld()) {
 432			try {
 433				wakeLock.release();
 434			} catch (final RuntimeException ignored) {
 435			}
 436		}
 437		return START_STICKY;
 438	}
 439
 440	public boolean hasInternetConnection() {
 441		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
 442				.getSystemService(Context.CONNECTIVITY_SERVICE);
 443		NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
 444		return activeNetwork != null && activeNetwork.isConnected();
 445	}
 446
 447	@SuppressLint("TrulyRandom")
 448	@Override
 449	public void onCreate() {
 450		ExceptionHelper.init(getApplicationContext());
 451		PRNGFixes.apply();
 452		this.mRandom = new SecureRandom();
 453		this.mMemorizingTrustManager = new MemorizingTrustManager(
 454				getApplicationContext());
 455
 456		int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
 457		int cacheSize = maxMemory / 8;
 458		this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
 459			@Override
 460			protected int sizeOf(String key, Bitmap bitmap) {
 461				return bitmap.getByteCount() / 1024;
 462			}
 463		};
 464
 465		this.databaseBackend = DatabaseBackend
 466				.getInstance(getApplicationContext());
 467		this.accounts = databaseBackend.getAccounts();
 468
 469		for (Account account : this.accounts) {
 470			this.databaseBackend.readRoster(account.getRoster());
 471		}
 472		this.mergePhoneContactsWithRoster();
 473		this.getConversations();
 474
 475		getContentResolver().registerContentObserver(
 476				ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
 477		this.fileObserver.startWatching();
 478		this.pgpServiceConnection = new OpenPgpServiceConnection(
 479				getApplicationContext(), "org.sufficientlysecure.keychain");
 480		this.pgpServiceConnection.bindToService();
 481
 482		this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 483		this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
 484				"XmppConnectionService");
 485	}
 486
 487	@Override
 488	public void onDestroy() {
 489		super.onDestroy();
 490		this.logoutAndSave();
 491	}
 492
 493	@Override
 494	public void onTaskRemoved(Intent rootIntent) {
 495		super.onTaskRemoved(rootIntent);
 496		this.logoutAndSave();
 497	}
 498
 499	private void logoutAndSave() {
 500		for (Account account : accounts) {
 501			databaseBackend.writeRoster(account.getRoster());
 502			if (account.getXmppConnection() != null) {
 503				disconnect(account, false);
 504			}
 505		}
 506		Context context = getApplicationContext();
 507		AlarmManager alarmManager = (AlarmManager) context
 508				.getSystemService(Context.ALARM_SERVICE);
 509		Intent intent = new Intent(context, EventReceiver.class);
 510		alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
 511		Log.d(Config.LOGTAG, "good bye");
 512		stopSelf();
 513	}
 514
 515	protected void scheduleWakeupCall(int seconds, boolean ping) {
 516		long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
 517		Context context = getApplicationContext();
 518		AlarmManager alarmManager = (AlarmManager) context
 519				.getSystemService(Context.ALARM_SERVICE);
 520
 521		if (ping) {
 522			if (this.pingIntent == null) {
 523				this.pingIntent = new Intent(context, EventReceiver.class);
 524				this.pingIntent.setAction("ping");
 525				this.pingIntent.putExtra("time", timeToWake);
 526				this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
 527						this.pingIntent, 0);
 528				alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 529						timeToWake, pendingPingIntent);
 530			} else {
 531				long scheduledTime = this.pingIntent.getLongExtra("time", 0);
 532				if (scheduledTime < SystemClock.elapsedRealtime()
 533						|| (scheduledTime > timeToWake)) {
 534					this.pingIntent.putExtra("time", timeToWake);
 535					alarmManager.cancel(this.pendingPingIntent);
 536					this.pendingPingIntent = PendingIntent.getBroadcast(
 537							context, 0, this.pingIntent, 0);
 538					alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 539							timeToWake, pendingPingIntent);
 540				}
 541			}
 542		} else {
 543			Intent intent = new Intent(context, EventReceiver.class);
 544			intent.setAction("ping_check");
 545			PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
 546					intent, 0);
 547			alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake,
 548					alarmIntent);
 549		}
 550
 551	}
 552
 553	public XmppConnection createConnection(Account account) {
 554		SharedPreferences sharedPref = getPreferences();
 555		account.setResource(sharedPref.getString("resource", "mobile")
 556				.toLowerCase(Locale.getDefault()));
 557		XmppConnection connection = new XmppConnection(account, this);
 558		connection.setOnMessagePacketReceivedListener(this.mMessageParser);
 559		connection.setOnStatusChangedListener(this.statusListener);
 560		connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
 561		connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
 562		connection.setOnJinglePacketReceivedListener(this.jingleListener);
 563		connection.setOnBindListener(this.mOnBindListener);
 564		connection
 565				.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
 566		return connection;
 567	}
 568
 569	public void sendMessage(Message message) {
 570		Account account = message.getConversation().getAccount();
 571		account.deactivateGracePeriod();
 572		Conversation conv = message.getConversation();
 573		MessagePacket packet = null;
 574		boolean saveInDb = true;
 575		boolean send = false;
 576		if (account.getStatus() == Account.STATUS_ONLINE
 577				&& account.getXmppConnection() != null) {
 578			if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
 579				if (message.getCounterpart() != null) {
 580					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 581						if (!conv.hasValidOtrSession()) {
 582							conv.startOtrSession(this, message.getCounterpart().getResourcepart(),
 583									true);
 584							message.setStatus(Message.STATUS_WAITING);
 585						} else if (conv.hasValidOtrSession()
 586								&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 587							mJingleConnectionManager
 588									.createNewConnection(message);
 589						}
 590					} else {
 591						mJingleConnectionManager.createNewConnection(message);
 592					}
 593				} else {
 594					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 595						conv.startOtrIfNeeded();
 596					}
 597					message.setStatus(Message.STATUS_WAITING);
 598				}
 599			} else {
 600				if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 601					if (!conv.hasValidOtrSession() && (message.getCounterpart() != null)) {
 602						conv.startOtrSession(this, message.getCounterpart().getResourcepart(), true);
 603						message.setStatus(Message.STATUS_WAITING);
 604					} else if (conv.hasValidOtrSession()) {
 605						if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 606							packet = mMessageGenerator.generateOtrChat(message);
 607							send = true;
 608						} else {
 609							message.setStatus(Message.STATUS_WAITING);
 610							conv.startOtrIfNeeded();
 611						}
 612					} else {
 613						message.setStatus(Message.STATUS_WAITING);
 614					}
 615				} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 616					message.getConversation().endOtrIfNeeded();
 617					failWaitingOtrMessages(message.getConversation());
 618					packet = mMessageGenerator.generatePgpChat(message);
 619					send = true;
 620				} else {
 621					message.getConversation().endOtrIfNeeded();
 622					failWaitingOtrMessages(message.getConversation());
 623					packet = mMessageGenerator.generateChat(message);
 624					send = true;
 625				}
 626			}
 627			if (!account.getXmppConnection().getFeatures().sm()
 628					&& conv.getMode() != Conversation.MODE_MULTI) {
 629				message.setStatus(Message.STATUS_SEND);
 630			}
 631		} else {
 632			message.setStatus(Message.STATUS_WAITING);
 633			if (message.getType() == Message.TYPE_TEXT) {
 634				if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 635					String pgpBody = message.getEncryptedBody();
 636					String decryptedBody = message.getBody();
 637					message.setBody(pgpBody);
 638					message.setEncryption(Message.ENCRYPTION_PGP);
 639					databaseBackend.createMessage(message);
 640					saveInDb = false;
 641					message.setBody(decryptedBody);
 642					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 643				} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 644					if (!conv.hasValidOtrSession()
 645							&& message.getCounterpart() != null) {
 646						conv.startOtrSession(this, message.getCounterpart().getResourcepart(), false);
 647					}
 648				}
 649			}
 650
 651		}
 652		conv.add(message);
 653		if (saveInDb) {
 654			if (message.getEncryption() == Message.ENCRYPTION_NONE
 655					|| saveEncryptedMessages()) {
 656				databaseBackend.createMessage(message);
 657			}
 658		}
 659		if ((send) && (packet != null)) {
 660			sendMessagePacket(account, packet);
 661		}
 662		updateConversationUi();
 663	}
 664
 665	private void sendUnsendMessages(Conversation conversation) {
 666		for (int i = 0; i < conversation.getMessages().size(); ++i) {
 667			int status = conversation.getMessages().get(i).getStatus();
 668			if (status == Message.STATUS_WAITING) {
 669				resendMessage(conversation.getMessages().get(i));
 670			}
 671		}
 672	}
 673
 674	private void resendMessage(Message message) {
 675		Account account = message.getConversation().getAccount();
 676		MessagePacket packet = null;
 677		if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 678			Presences presences = message.getConversation().getContact()
 679					.getPresences();
 680			if (!message.getConversation().hasValidOtrSession()) {
 681				if ((message.getCounterpart() != null)
 682						&& (presences.has(message.getCounterpart().getResourcepart()))) {
 683					message.getConversation().startOtrSession(this,
 684							message.getCounterpart().getResourcepart(), true);
 685				} else {
 686					if (presences.size() == 1) {
 687						String presence = presences.asStringArray()[0];
 688						message.getConversation().startOtrSession(this,
 689								presence, true);
 690					}
 691				}
 692			} else {
 693				if (message.getConversation().getOtrSession()
 694						.getSessionStatus() == SessionStatus.ENCRYPTED) {
 695					if (message.getType() == Message.TYPE_TEXT) {
 696						packet = mMessageGenerator.generateOtrChat(message,
 697								true);
 698					} else if (message.getType() == Message.TYPE_IMAGE) {
 699						mJingleConnectionManager.createNewConnection(message);
 700					}
 701				}
 702			}
 703		} else if (message.getType() == Message.TYPE_TEXT) {
 704			if (message.getEncryption() == Message.ENCRYPTION_NONE) {
 705				packet = mMessageGenerator.generateChat(message, true);
 706			} else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
 707					|| (message.getEncryption() == Message.ENCRYPTION_PGP)) {
 708				packet = mMessageGenerator.generatePgpChat(message, true);
 709			}
 710		} else if (message.getType() == Message.TYPE_IMAGE) {
 711			Contact contact = message.getConversation().getContact();
 712			Presences presences = contact.getPresences();
 713			if ((message.getCounterpart() != null)
 714					&& (presences.has(message.getCounterpart().getResourcepart()))) {
 715				markMessage(message, Message.STATUS_OFFERED);
 716				mJingleConnectionManager.createNewConnection(message);
 717			} else {
 718				if (presences.size() == 1) {
 719					String presence = presences.asStringArray()[0];
 720					try {
 721						message.setCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence));
 722					} catch (InvalidJidException e) {
 723						return;
 724					}
 725					markMessage(message, Message.STATUS_OFFERED);
 726					mJingleConnectionManager.createNewConnection(message);
 727				}
 728			}
 729		}
 730		if (packet != null) {
 731			if (!account.getXmppConnection().getFeatures().sm()
 732					&& message.getConversation().getMode() != Conversation.MODE_MULTI) {
 733				markMessage(message, Message.STATUS_SEND);
 734			} else {
 735				markMessage(message, Message.STATUS_UNSEND);
 736			}
 737			sendMessagePacket(account, packet);
 738		}
 739	}
 740
 741	public void fetchRosterFromServer(Account account) {
 742		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
 743		if (!"".equals(account.getRosterVersion())) {
 744			Log.d(Config.LOGTAG, account.getJid().toBareJid()
 745					+ ": fetching roster version " + account.getRosterVersion());
 746		} else {
 747			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
 748		}
 749		iqPacket.query("jabber:iq:roster").setAttribute("ver",
 750				account.getRosterVersion());
 751		account.getXmppConnection().sendIqPacket(iqPacket,
 752				new OnIqPacketReceived() {
 753
 754					@Override
 755					public void onIqPacketReceived(final Account account,
 756												   IqPacket packet) {
 757						Element query = packet.findChild("query");
 758						if (query != null) {
 759							account.getRoster().markAllAsNotInRoster();
 760							mIqParser.rosterItems(account, query);
 761						}
 762					}
 763				});
 764	}
 765
 766	public void fetchBookmarks(Account account) {
 767		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
 768		Element query = iqPacket.query("jabber:iq:private");
 769		query.addChild("storage", "storage:bookmarks");
 770		OnIqPacketReceived callback = new OnIqPacketReceived() {
 771
 772			@Override
 773			public void onIqPacketReceived(Account account, IqPacket packet) {
 774				Element query = packet.query();
 775				List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
 776				Element storage = query.findChild("storage",
 777						"storage:bookmarks");
 778				if (storage != null) {
 779					for (Element item : storage.getChildren()) {
 780						if (item.getName().equals("conference")) {
 781							Bookmark bookmark = Bookmark.parse(item, account);
 782							bookmarks.add(bookmark);
 783							Conversation conversation = find(bookmark);
 784							if (conversation != null) {
 785								conversation.setBookmark(bookmark);
 786							} else {
 787								if (bookmark.autojoin()) {
 788									conversation = findOrCreateConversation(
 789											account, bookmark.getJid(), true);
 790									conversation.setBookmark(bookmark);
 791									joinMuc(conversation);
 792								}
 793							}
 794						}
 795					}
 796				}
 797				account.setBookmarks(bookmarks);
 798			}
 799		};
 800		sendIqPacket(account, iqPacket, callback);
 801
 802	}
 803
 804	public void pushBookmarks(Account account) {
 805		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_SET);
 806		Element query = iqPacket.query("jabber:iq:private");
 807		Element storage = query.addChild("storage", "storage:bookmarks");
 808		for (Bookmark bookmark : account.getBookmarks()) {
 809			storage.addChild(bookmark);
 810		}
 811		sendIqPacket(account, iqPacket, null);
 812	}
 813
 814	private void mergePhoneContactsWithRoster() {
 815		PhoneHelper.loadPhoneContacts(getApplicationContext(),
 816				new OnPhoneContactsLoadedListener() {
 817					@Override
 818					public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
 819						for (Account account : accounts) {
 820							account.getRoster().clearSystemAccounts();
 821						}
 822						for (Bundle phoneContact : phoneContacts) {
 823							for (Account account : accounts) {
 824								Jid jid;
 825								try {
 826									jid = Jid.fromString(phoneContact.getString("jid"));
 827								} catch (final InvalidJidException e) {
 828									// TODO: Warn if contact import fails here?
 829									break;
 830								}
 831								final Contact contact = account.getRoster()
 832										.getContact(jid);
 833								String systemAccount = phoneContact
 834										.getInt("phoneid")
 835										+ "#"
 836										+ phoneContact.getString("lookup");
 837								contact.setSystemAccount(systemAccount);
 838								contact.setPhotoUri(phoneContact
 839										.getString("photouri"));
 840								contact.setSystemName(phoneContact
 841										.getString("displayname"));
 842								getAvatarService().clear(contact);
 843							}
 844						}
 845					}
 846				});
 847	}
 848
 849	public List<Conversation> getConversations() {
 850		if (this.conversations == null) {
 851			Hashtable<String, Account> accountLookupTable = new Hashtable<>();
 852			for (Account account : this.accounts) {
 853				accountLookupTable.put(account.getUuid(), account);
 854			}
 855			this.conversations = databaseBackend
 856					.getConversations(Conversation.STATUS_AVAILABLE);
 857			for (Conversation conv : this.conversations) {
 858				Account account = accountLookupTable.get(conv.getAccountUuid());
 859				conv.setAccount(account);
 860				conv.setMessages(databaseBackend.getMessages(conv, 50));
 861				checkDeletedFiles(conv);
 862			}
 863		}
 864		return this.conversations;
 865	}
 866
 867	private void checkDeletedFiles(Conversation conversation) {
 868		for (Message message : conversation.getMessages()) {
 869			if (message.getType() == Message.TYPE_IMAGE
 870					&& message.getEncryption() != Message.ENCRYPTION_PGP) {
 871				if (!getFileBackend().isFileAvailable(message)) {
 872					message.setDownloadable(new DeletedDownloadable());
 873				}
 874			}
 875		}
 876	}
 877
 878	private void markFileDeleted(String uuid) {
 879		for (Conversation conversation : getConversations()) {
 880			for (Message message : conversation.getMessages()) {
 881				if (message.getType() == Message.TYPE_IMAGE
 882						&& message.getEncryption() != Message.ENCRYPTION_PGP
 883						&& message.getUuid().equals(uuid)) {
 884					if (!getFileBackend().isFileAvailable(message)) {
 885						message.setDownloadable(new DeletedDownloadable());
 886						updateConversationUi();
 887					}
 888					return;
 889				}
 890			}
 891		}
 892	}
 893
 894	public void populateWithOrderedConversations(List<Conversation> list) {
 895		populateWithOrderedConversations(list, true);
 896	}
 897
 898	public void populateWithOrderedConversations(List<Conversation> list,
 899												 boolean includeConferences) {
 900		list.clear();
 901		if (includeConferences) {
 902			list.addAll(getConversations());
 903		} else {
 904			for (Conversation conversation : getConversations()) {
 905				if (conversation.getMode() == Conversation.MODE_SINGLE) {
 906					list.add(conversation);
 907				}
 908			}
 909		}
 910		Collections.sort(list, new Comparator<Conversation>() {
 911			@Override
 912			public int compare(Conversation lhs, Conversation rhs) {
 913				Message left = lhs.getLatestMessage();
 914				Message right = rhs.getLatestMessage();
 915				if (left.getTimeSent() > right.getTimeSent()) {
 916					return -1;
 917				} else if (left.getTimeSent() < right.getTimeSent()) {
 918					return 1;
 919				} else {
 920					return 0;
 921				}
 922			}
 923		});
 924	}
 925
 926	public int loadMoreMessages(Conversation conversation, long timestamp) {
 927		List<Message> messages = databaseBackend.getMessages(conversation, 50,
 928				timestamp);
 929		for (Message message : messages) {
 930			message.setConversation(conversation);
 931		}
 932		conversation.addAll(0, messages);
 933		return messages.size();
 934	}
 935
 936	public List<Account> getAccounts() {
 937		return this.accounts;
 938	}
 939
 940	public Conversation find(List<Conversation> haystack, Contact contact) {
 941		for (Conversation conversation : haystack) {
 942			if (conversation.getContact() == contact) {
 943				return conversation;
 944			}
 945		}
 946		return null;
 947	}
 948
 949	public Conversation find(final List<Conversation> haystack,
 950							 final Account account,
 951							 final Jid jid) {
 952		for (Conversation conversation : haystack) {
 953			if ((account == null || conversation.getAccount() == account)
 954					&& (conversation.getContactJid().toBareJid().equals(jid.toBareJid()))) {
 955				return conversation;
 956			}
 957		}
 958		return null;
 959	}
 960
 961	public Conversation findOrCreateConversation(final Account account, final Jid jid,
 962												 final boolean muc) {
 963		Conversation conversation = find(account, jid);
 964		if (conversation != null) {
 965			return conversation;
 966		}
 967		conversation = databaseBackend.findConversation(account, jid);
 968		if (conversation != null) {
 969			conversation.setStatus(Conversation.STATUS_AVAILABLE);
 970			conversation.setAccount(account);
 971			if (muc) {
 972				conversation.setMode(Conversation.MODE_MULTI);
 973			} else {
 974				conversation.setMode(Conversation.MODE_SINGLE);
 975			}
 976			conversation.setMessages(databaseBackend.getMessages(conversation,
 977					50));
 978			this.databaseBackend.updateConversation(conversation);
 979		} else {
 980			String conversationName;
 981			Contact contact = account.getRoster().getContact(jid);
 982			if (contact != null) {
 983				conversationName = contact.getDisplayName();
 984			} else {
 985				conversationName = jid.getLocalpart();
 986			}
 987			if (muc) {
 988				conversation = new Conversation(conversationName, account, jid,
 989						Conversation.MODE_MULTI);
 990			} else {
 991				conversation = new Conversation(conversationName, account, jid,
 992						Conversation.MODE_SINGLE);
 993			}
 994			this.databaseBackend.createConversation(conversation);
 995		}
 996		this.conversations.add(conversation);
 997		updateConversationUi();
 998		return conversation;
 999	}
1000
1001	public void archiveConversation(Conversation conversation) {
1002		if (conversation.getMode() == Conversation.MODE_MULTI) {
1003			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1004				Bookmark bookmark = conversation.getBookmark();
1005				if (bookmark != null && bookmark.autojoin()) {
1006					bookmark.setAutojoin(false);
1007					pushBookmarks(bookmark.getAccount());
1008				}
1009			}
1010			leaveMuc(conversation);
1011		} else {
1012			conversation.endOtrIfNeeded();
1013		}
1014		this.databaseBackend.updateConversation(conversation);
1015		this.conversations.remove(conversation);
1016		updateConversationUi();
1017	}
1018
1019	public void clearConversationHistory(Conversation conversation) {
1020		this.databaseBackend.deleteMessagesInConversation(conversation);
1021		conversation.getMessages().clear();
1022		updateConversationUi();
1023	}
1024
1025	public int getConversationCount() {
1026		return this.databaseBackend.getConversationCount();
1027	}
1028
1029	public void createAccount(Account account) {
1030		databaseBackend.createAccount(account);
1031		this.accounts.add(account);
1032		this.reconnectAccount(account, false);
1033		updateAccountUi();
1034	}
1035
1036	public void updateAccount(Account account) {
1037		this.statusListener.onStatusChanged(account);
1038		databaseBackend.updateAccount(account);
1039		reconnectAccount(account, false);
1040		updateAccountUi();
1041		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1042	}
1043
1044	public void deleteAccount(Account account) {
1045		for (Conversation conversation : conversations) {
1046			if (conversation.getAccount() == account) {
1047				if (conversation.getMode() == Conversation.MODE_MULTI) {
1048					leaveMuc(conversation);
1049				} else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1050					conversation.endOtrIfNeeded();
1051				}
1052				conversations.remove(conversation);
1053			}
1054		}
1055		if (account.getXmppConnection() != null) {
1056			this.disconnect(account, true);
1057		}
1058		databaseBackend.deleteAccount(account);
1059		this.accounts.remove(account);
1060		updateAccountUi();
1061		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1062	}
1063
1064	private void removeStaleListeners() {
1065		boolean removedListener = false;
1066		synchronized (this.convChangedListenerCount) {
1067			if (this.mOnConversationUpdate != null) {
1068				this.mOnConversationUpdate = null;
1069				this.convChangedListenerCount = 0;
1070				this.mNotificationService.setIsInForeground(false);
1071				removedListener = true;
1072			}
1073		}
1074		synchronized (this.accountChangedListenerCount) {
1075			if (this.mOnAccountUpdate != null) {
1076				this.mOnAccountUpdate = null;
1077				this.accountChangedListenerCount = 0;
1078				removedListener = true;
1079			}
1080		}
1081		synchronized (this.rosterChangedListenerCount) {
1082			if (this.mOnRosterUpdate != null) {
1083				this.mOnRosterUpdate = null;
1084				this.rosterChangedListenerCount = 0;
1085				removedListener = true;
1086			}
1087		}
1088		if (removedListener) {
1089			final String msg = "removed stale listeners";
1090			Log.d(Config.LOGTAG, msg);
1091			checkListeners();
1092			try {
1093				OutputStream os = openFileOutput("stacktrace.txt", MODE_PRIVATE);
1094				os.write(msg.getBytes());
1095				os.flush();
1096				os.close();
1097			} catch (final FileNotFoundException ignored) {
1098
1099			} catch (final IOException ignored) {
1100			}
1101		}
1102	}
1103
1104	public void setOnConversationListChangedListener(
1105			OnConversationUpdate listener) {
1106		/*if (!isScreenOn()) {
1107			Log.d(Config.LOGTAG,
1108					"ignoring setOnConversationListChangedListener");
1109			return;
1110		}*/
1111		synchronized (this.convChangedListenerCount) {
1112			if (checkListeners()) {
1113				switchToForeground();
1114			}
1115			this.mOnConversationUpdate = listener;
1116			this.mNotificationService.setIsInForeground(true);
1117			this.convChangedListenerCount++;
1118		}
1119	}
1120
1121	public void removeOnConversationListChangedListener() {
1122		synchronized (this.convChangedListenerCount) {
1123			this.convChangedListenerCount--;
1124			if (this.convChangedListenerCount <= 0) {
1125				this.convChangedListenerCount = 0;
1126				this.mOnConversationUpdate = null;
1127				this.mNotificationService.setIsInForeground(false);
1128				if (checkListeners()) {
1129					switchToBackground();
1130				}
1131			}
1132		}
1133	}
1134
1135	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1136		/*if (!isScreenOn()) {
1137			Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener");
1138			return;
1139		}*/
1140		synchronized (this.accountChangedListenerCount) {
1141			if (checkListeners()) {
1142				switchToForeground();
1143			}
1144			this.mOnAccountUpdate = listener;
1145			this.accountChangedListenerCount++;
1146		}
1147	}
1148
1149	public void removeOnAccountListChangedListener() {
1150		synchronized (this.accountChangedListenerCount) {
1151			this.accountChangedListenerCount--;
1152			if (this.accountChangedListenerCount <= 0) {
1153				this.mOnAccountUpdate = null;
1154				this.accountChangedListenerCount = 0;
1155				if (checkListeners()) {
1156					switchToBackground();
1157				}
1158			}
1159		}
1160	}
1161
1162	public void setOnRosterUpdateListener(OnRosterUpdate listener) {
1163		/*if (!isScreenOn()) {
1164			Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener");
1165			return;
1166		}*/
1167		synchronized (this.rosterChangedListenerCount) {
1168			if (checkListeners()) {
1169				switchToForeground();
1170			}
1171			this.mOnRosterUpdate = listener;
1172			this.rosterChangedListenerCount++;
1173		}
1174	}
1175
1176	public void removeOnRosterUpdateListener() {
1177		synchronized (this.rosterChangedListenerCount) {
1178			this.rosterChangedListenerCount--;
1179			if (this.rosterChangedListenerCount <= 0) {
1180				this.rosterChangedListenerCount = 0;
1181				this.mOnRosterUpdate = null;
1182				if (checkListeners()) {
1183					switchToBackground();
1184				}
1185			}
1186		}
1187	}
1188
1189	private boolean checkListeners() {
1190		return (this.mOnAccountUpdate == null
1191				&& this.mOnConversationUpdate == null && this.mOnRosterUpdate == null);
1192	}
1193
1194	private void switchToForeground() {
1195		for (Account account : getAccounts()) {
1196			if (account.getStatus() == Account.STATUS_ONLINE) {
1197				XmppConnection connection = account.getXmppConnection();
1198				if (connection != null && connection.getFeatures().csi()) {
1199					connection.sendActive();
1200				}
1201			}
1202		}
1203		Log.d(Config.LOGTAG, "app switched into foreground");
1204	}
1205
1206	private void switchToBackground() {
1207		for (Account account : getAccounts()) {
1208			if (account.getStatus() == Account.STATUS_ONLINE) {
1209				XmppConnection connection = account.getXmppConnection();
1210				if (connection != null && connection.getFeatures().csi()) {
1211					connection.sendInactive();
1212				}
1213			}
1214		}
1215		this.mNotificationService.setIsInForeground(false);
1216		Log.d(Config.LOGTAG, "app switched into background");
1217	}
1218
1219	private boolean isScreenOn() {
1220		PowerManager pm = (PowerManager) this
1221				.getSystemService(Context.POWER_SERVICE);
1222		return pm.isScreenOn();
1223	}
1224
1225	public void connectMultiModeConversations(Account account) {
1226		List<Conversation> conversations = getConversations();
1227		for (Conversation conversation : conversations) {
1228			if ((conversation.getMode() == Conversation.MODE_MULTI)
1229					&& (conversation.getAccount() == account)) {
1230				joinMuc(conversation);
1231			}
1232		}
1233	}
1234
1235	public void joinMuc(Conversation conversation) {
1236		Account account = conversation.getAccount();
1237		account.pendingConferenceJoins.remove(conversation);
1238		account.pendingConferenceLeaves.remove(conversation);
1239		if (account.getStatus() == Account.STATUS_ONLINE) {
1240			Log.d(Config.LOGTAG,
1241					"joining conversation " + conversation.getContactJid());
1242			String nick = conversation.getMucOptions().getProposedNick();
1243			conversation.getMucOptions().setJoinNick(nick);
1244			PresencePacket packet = new PresencePacket();
1245			final Jid joinJid = conversation.getMucOptions().getJoinJid();
1246			packet.setTo(conversation.getMucOptions().getJoinJid());
1247			Element x = new Element("x");
1248			x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
1249			if (conversation.getMucOptions().getPassword() != null) {
1250				Element password = x.addChild("password");
1251				password.setContent(conversation.getMucOptions().getPassword());
1252			}
1253			String sig = account.getPgpSignature();
1254			if (sig != null) {
1255				packet.addChild("status").setContent("online");
1256				packet.addChild("x", "jabber:x:signed").setContent(sig);
1257			}
1258			if (conversation.getMessages().size() != 0) {
1259				final SimpleDateFormat mDateFormat = new SimpleDateFormat(
1260						"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
1261				mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
1262				Date date = new Date(conversation.getLatestMessage()
1263						.getTimeSent() + 1000);
1264				x.addChild("history").setAttribute("since",
1265						mDateFormat.format(date));
1266			}
1267			packet.addChild(x);
1268			sendPresencePacket(account, packet);
1269			if (!joinJid.equals(conversation.getContactJid())) {
1270				conversation.setContactJid(joinJid);
1271				databaseBackend.updateConversation(conversation);
1272			}
1273		} else {
1274			account.pendingConferenceJoins.add(conversation);
1275		}
1276	}
1277
1278	public void setOnRenameListener(OnRenameListener listener) {
1279		this.renameListener = listener;
1280	}
1281
1282	public void providePasswordForMuc(Conversation conversation, String password) {
1283		if (conversation.getMode() == Conversation.MODE_MULTI) {
1284			conversation.getMucOptions().setPassword(password);
1285			if (conversation.getBookmark() != null) {
1286				conversation.getBookmark().setAutojoin(true);
1287				pushBookmarks(conversation.getAccount());
1288			}
1289			databaseBackend.updateConversation(conversation);
1290			joinMuc(conversation);
1291		}
1292	}
1293
1294	public void renameInMuc(final Conversation conversation, final String nick) {
1295		final MucOptions options = conversation.getMucOptions();
1296		options.setJoinNick(nick);
1297		if (options.online()) {
1298			Account account = conversation.getAccount();
1299			options.setOnRenameListener(new OnRenameListener() {
1300
1301				@Override
1302				public void onRename(boolean success) {
1303					if (renameListener != null) {
1304						renameListener.onRename(success);
1305					}
1306					if (success) {
1307						conversation.setContactJid(conversation.getMucOptions()
1308								.getJoinJid());
1309						databaseBackend.updateConversation(conversation);
1310						Bookmark bookmark = conversation.getBookmark();
1311						if (bookmark != null) {
1312							bookmark.setNick(nick);
1313							pushBookmarks(bookmark.getAccount());
1314						}
1315					}
1316				}
1317			});
1318			options.flagAboutToRename();
1319			PresencePacket packet = new PresencePacket();
1320			packet.setTo(options.getJoinJid());
1321			packet.setFrom(conversation.getAccount().getJid());
1322
1323			String sig = account.getPgpSignature();
1324			if (sig != null) {
1325				packet.addChild("status").setContent("online");
1326				packet.addChild("x", "jabber:x:signed").setContent(sig);
1327			}
1328			sendPresencePacket(account, packet);
1329		} else {
1330			conversation.setContactJid(options.getJoinJid());
1331			databaseBackend.updateConversation(conversation);
1332			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1333				Bookmark bookmark = conversation.getBookmark();
1334				if (bookmark != null) {
1335					bookmark.setNick(nick);
1336					pushBookmarks(bookmark.getAccount());
1337				}
1338				joinMuc(conversation);
1339			}
1340		}
1341	}
1342
1343	public void leaveMuc(Conversation conversation) {
1344		Account account = conversation.getAccount();
1345		account.pendingConferenceJoins.remove(conversation);
1346		account.pendingConferenceLeaves.remove(conversation);
1347		if (account.getStatus() == Account.STATUS_ONLINE) {
1348			PresencePacket packet = new PresencePacket();
1349			packet.setTo(conversation.getMucOptions().getJoinJid());
1350			packet.setFrom(conversation.getAccount().getJid());
1351			packet.setAttribute("type", "unavailable");
1352			sendPresencePacket(conversation.getAccount(), packet);
1353			conversation.getMucOptions().setOffline();
1354			conversation.deregisterWithBookmark();
1355			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1356					+ ": leaving muc " + conversation.getContactJid());
1357		} else {
1358			account.pendingConferenceLeaves.add(conversation);
1359		}
1360	}
1361
1362	public void disconnect(Account account, boolean force) {
1363		if ((account.getStatus() == Account.STATUS_ONLINE)
1364				|| (account.getStatus() == Account.STATUS_DISABLED)) {
1365			if (!force) {
1366				List<Conversation> conversations = getConversations();
1367				for (Conversation conversation : conversations) {
1368					if (conversation.getAccount() == account) {
1369						if (conversation.getMode() == Conversation.MODE_MULTI) {
1370							leaveMuc(conversation);
1371						} else {
1372							if (conversation.endOtrIfNeeded()) {
1373								Log.d(Config.LOGTAG, account.getJid().toBareJid()
1374										+ ": ended otr session with "
1375										+ conversation.getContactJid());
1376							}
1377						}
1378					}
1379				}
1380			}
1381			account.getXmppConnection().disconnect(force);
1382		}
1383	}
1384
1385	@Override
1386	public IBinder onBind(Intent intent) {
1387		return mBinder;
1388	}
1389
1390	public void updateMessage(Message message) {
1391		databaseBackend.updateMessage(message);
1392		updateConversationUi();
1393	}
1394
1395	protected void syncDirtyContacts(Account account) {
1396		for (Contact contact : account.getRoster().getContacts()) {
1397			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1398				pushContactToServer(contact);
1399			}
1400			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1401				deleteContactOnServer(contact);
1402			}
1403		}
1404	}
1405
1406	public void createContact(Contact contact) {
1407		SharedPreferences sharedPref = getPreferences();
1408		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1409		if (autoGrant) {
1410			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1411			contact.setOption(Contact.Options.ASKING);
1412		}
1413		pushContactToServer(contact);
1414	}
1415
1416	public void onOtrSessionEstablished(Conversation conversation) {
1417		Account account = conversation.getAccount();
1418		List<Message> messages = conversation.getMessages();
1419		Session otrSession = conversation.getOtrSession();
1420		Log.d(Config.LOGTAG,
1421				account.getJid().toBareJid() + " otr session established with "
1422						+ conversation.getContactJid() + "/"
1423						+ otrSession.getSessionID().getUserID());
1424		for (Message msg : messages) {
1425			if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING)
1426					&& (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
1427				SessionID id = otrSession.getSessionID();
1428				try {
1429					msg.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
1430				} catch (InvalidJidException e) {
1431					break;
1432				}
1433				if (msg.getType() == Message.TYPE_TEXT) {
1434					MessagePacket outPacket = mMessageGenerator
1435							.generateOtrChat(msg, true);
1436					if (outPacket != null) {
1437						msg.setStatus(Message.STATUS_SEND);
1438						databaseBackend.updateMessage(msg);
1439						sendMessagePacket(account, outPacket);
1440					}
1441				} else if (msg.getType() == Message.TYPE_IMAGE) {
1442					mJingleConnectionManager.createNewConnection(msg);
1443				}
1444			}
1445		}
1446		updateConversationUi();
1447	}
1448
1449	public boolean renewSymmetricKey(Conversation conversation) {
1450		Account account = conversation.getAccount();
1451		byte[] symmetricKey = new byte[32];
1452		this.mRandom.nextBytes(symmetricKey);
1453		Session otrSession = conversation.getOtrSession();
1454		if (otrSession != null) {
1455			MessagePacket packet = new MessagePacket();
1456			packet.setType(MessagePacket.TYPE_CHAT);
1457			packet.setFrom(account.getJid());
1458			packet.addChild("private", "urn:xmpp:carbons:2");
1459			packet.addChild("no-copy", "urn:xmpp:hints");
1460			packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
1461					+ otrSession.getSessionID().getUserID());
1462			try {
1463				packet.setBody(otrSession
1464						.transformSending(CryptoHelper.FILETRANSFER
1465								+ CryptoHelper.bytesToHex(symmetricKey)));
1466				sendMessagePacket(account, packet);
1467				conversation.setSymmetricKey(symmetricKey);
1468				return true;
1469			} catch (OtrException e) {
1470				return false;
1471			}
1472		}
1473		return false;
1474	}
1475
1476	public void pushContactToServer(Contact contact) {
1477		contact.resetOption(Contact.Options.DIRTY_DELETE);
1478		contact.setOption(Contact.Options.DIRTY_PUSH);
1479		Account account = contact.getAccount();
1480		if (account.getStatus() == Account.STATUS_ONLINE) {
1481			boolean ask = contact.getOption(Contact.Options.ASKING);
1482			boolean sendUpdates = contact
1483					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
1484					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
1485			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1486			iq.query("jabber:iq:roster").addChild(contact.asElement());
1487			account.getXmppConnection().sendIqPacket(iq, null);
1488			if (sendUpdates) {
1489				sendPresencePacket(account,
1490						mPresenceGenerator.sendPresenceUpdatesTo(contact));
1491			}
1492			if (ask) {
1493				sendPresencePacket(account,
1494						mPresenceGenerator.requestPresenceUpdatesFrom(contact));
1495			}
1496		}
1497	}
1498
1499	public void publishAvatar(Account account, Uri image,
1500							  final UiCallback<Avatar> callback) {
1501		final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
1502		final int size = Config.AVATAR_SIZE;
1503		final Avatar avatar = getFileBackend()
1504				.getPepAvatar(image, size, format);
1505		if (avatar != null) {
1506			avatar.height = size;
1507			avatar.width = size;
1508			if (format.equals(Bitmap.CompressFormat.WEBP)) {
1509				avatar.type = "image/webp";
1510			} else if (format.equals(Bitmap.CompressFormat.JPEG)) {
1511				avatar.type = "image/jpeg";
1512			} else if (format.equals(Bitmap.CompressFormat.PNG)) {
1513				avatar.type = "image/png";
1514			}
1515			if (!getFileBackend().save(avatar)) {
1516				callback.error(R.string.error_saving_avatar, avatar);
1517				return;
1518			}
1519			IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
1520			this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1521
1522				@Override
1523				public void onIqPacketReceived(Account account, IqPacket result) {
1524					if (result.getType() == IqPacket.TYPE_RESULT) {
1525						IqPacket packet = XmppConnectionService.this.mIqGenerator
1526								.publishAvatarMetadata(avatar);
1527						sendIqPacket(account, packet, new OnIqPacketReceived() {
1528
1529							@Override
1530							public void onIqPacketReceived(Account account,
1531														   IqPacket result) {
1532								if (result.getType() == IqPacket.TYPE_RESULT) {
1533									if (account.setAvatar(avatar.getFilename())) {
1534										databaseBackend.updateAccount(account);
1535									}
1536									callback.success(avatar);
1537								} else {
1538									callback.error(
1539											R.string.error_publish_avatar_server_reject,
1540											avatar);
1541								}
1542							}
1543						});
1544					} else {
1545						callback.error(
1546								R.string.error_publish_avatar_server_reject,
1547								avatar);
1548					}
1549				}
1550			});
1551		} else {
1552			callback.error(R.string.error_publish_avatar_converting, null);
1553		}
1554	}
1555
1556	public void fetchAvatar(Account account, Avatar avatar) {
1557		fetchAvatar(account, avatar, null);
1558	}
1559
1560	public void fetchAvatar(Account account, final Avatar avatar,
1561							final UiCallback<Avatar> callback) {
1562		IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar);
1563		sendIqPacket(account, packet, new OnIqPacketReceived() {
1564
1565			@Override
1566			public void onIqPacketReceived(Account account, IqPacket result) {
1567				final String ERROR = account.getJid().toBareJid()
1568						+ ": fetching avatar for " + avatar.owner + " failed ";
1569				if (result.getType() == IqPacket.TYPE_RESULT) {
1570					avatar.image = mIqParser.avatarData(result);
1571					if (avatar.image != null) {
1572						if (getFileBackend().save(avatar)) {
1573							if (account.getJid().toBareJid().equals(avatar.owner)) {
1574								if (account.setAvatar(avatar.getFilename())) {
1575									databaseBackend.updateAccount(account);
1576								}
1577								getAvatarService().clear(account);
1578								updateConversationUi();
1579								updateAccountUi();
1580							} else {
1581								Contact contact = account.getRoster()
1582										.getContact(avatar.owner);
1583								contact.setAvatar(avatar.getFilename());
1584								getAvatarService().clear(contact);
1585								updateConversationUi();
1586								updateRosterUi();
1587							}
1588							if (callback != null) {
1589								callback.success(avatar);
1590							}
1591							Log.d(Config.LOGTAG, account.getJid().toBareJid()
1592									+ ": succesfully fetched avatar for "
1593									+ avatar.owner);
1594							return;
1595						}
1596					} else {
1597
1598						Log.d(Config.LOGTAG, ERROR + "(parsing error)");
1599					}
1600				} else {
1601					Element error = result.findChild("error");
1602					if (error == null) {
1603						Log.d(Config.LOGTAG, ERROR + "(server error)");
1604					} else {
1605						Log.d(Config.LOGTAG, ERROR + error.toString());
1606					}
1607				}
1608				if (callback != null) {
1609					callback.error(0, null);
1610				}
1611
1612			}
1613		});
1614	}
1615
1616	public void checkForAvatar(Account account,
1617							   final UiCallback<Avatar> callback) {
1618		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
1619		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1620
1621			@Override
1622			public void onIqPacketReceived(Account account, IqPacket packet) {
1623				if (packet.getType() == IqPacket.TYPE_RESULT) {
1624					Element pubsub = packet.findChild("pubsub",
1625							"http://jabber.org/protocol/pubsub");
1626					if (pubsub != null) {
1627						Element items = pubsub.findChild("items");
1628						if (items != null) {
1629							Avatar avatar = Avatar.parseMetadata(items);
1630							if (avatar != null) {
1631								avatar.owner = account.getJid().toBareJid();
1632								if (fileBackend.isAvatarCached(avatar)) {
1633									if (account.setAvatar(avatar.getFilename())) {
1634										databaseBackend.updateAccount(account);
1635									}
1636									getAvatarService().clear(account);
1637									callback.success(avatar);
1638								} else {
1639									fetchAvatar(account, avatar, callback);
1640								}
1641								return;
1642							}
1643						}
1644					}
1645				}
1646				callback.error(0, null);
1647			}
1648		});
1649	}
1650
1651	public void deleteContactOnServer(Contact contact) {
1652		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
1653		contact.resetOption(Contact.Options.DIRTY_PUSH);
1654		contact.setOption(Contact.Options.DIRTY_DELETE);
1655		Account account = contact.getAccount();
1656		if (account.getStatus() == Account.STATUS_ONLINE) {
1657			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1658			Element item = iq.query("jabber:iq:roster").addChild("item");
1659			item.setAttribute("jid", contact.getJid().toString());
1660			item.setAttribute("subscription", "remove");
1661			account.getXmppConnection().sendIqPacket(iq, null);
1662		}
1663	}
1664
1665	public void updateConversation(Conversation conversation) {
1666		this.databaseBackend.updateConversation(conversation);
1667	}
1668
1669	public void reconnectAccount(final Account account, final boolean force) {
1670		new Thread(new Runnable() {
1671
1672			@Override
1673			public void run() {
1674				if (account.getXmppConnection() != null) {
1675					disconnect(account, force);
1676				}
1677				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1678					if (account.getXmppConnection() == null) {
1679						account.setXmppConnection(createConnection(account));
1680					}
1681					Thread thread = new Thread(account.getXmppConnection());
1682					thread.start();
1683					scheduleWakeupCall((int) (Config.CONNECT_TIMEOUT * 1.2),
1684							false);
1685				} else {
1686					account.getRoster().clearPresences();
1687					account.setXmppConnection(null);
1688				}
1689			}
1690		}).start();
1691	}
1692
1693	public void invite(Conversation conversation, String contact) {
1694		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
1695		sendMessagePacket(conversation.getAccount(), packet);
1696	}
1697
1698	public void resetSendingToWaiting(Account account) {
1699		for (Conversation conversation : getConversations()) {
1700			if (conversation.getAccount() == account) {
1701				for (Message message : conversation.getMessages()) {
1702					if (message.getType() != Message.TYPE_IMAGE
1703							&& message.getStatus() == Message.STATUS_UNSEND) {
1704						markMessage(message, Message.STATUS_WAITING);
1705					}
1706				}
1707			}
1708		}
1709	}
1710
1711	public boolean markMessage(final Account account, final Jid recipient, final String uuid,
1712							   final int status) {
1713		if (uuid == null) {
1714			return false;
1715		} else {
1716			for (Conversation conversation : getConversations()) {
1717				if (conversation.getContactJid().equals(recipient)
1718						&& conversation.getAccount().equals(account)) {
1719					return markMessage(conversation, uuid, status);
1720				}
1721			}
1722			return false;
1723		}
1724	}
1725
1726	public boolean markMessage(Conversation conversation, String uuid,
1727							   int status) {
1728		if (uuid == null) {
1729			return false;
1730		} else {
1731			for (Message message : conversation.getMessages()) {
1732				if (uuid.equals(message.getUuid())
1733						|| (message.getStatus() >= Message.STATUS_SEND && uuid
1734						.equals(message.getRemoteMsgId()))) {
1735					markMessage(message, status);
1736					return true;
1737				}
1738			}
1739			return false;
1740		}
1741	}
1742
1743	public void markMessage(Message message, int status) {
1744		if (status == Message.STATUS_SEND_FAILED
1745				&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
1746				.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
1747			return;
1748		}
1749		message.setStatus(status);
1750		databaseBackend.updateMessage(message);
1751		updateConversationUi();
1752	}
1753
1754	public SharedPreferences getPreferences() {
1755		return PreferenceManager
1756				.getDefaultSharedPreferences(getApplicationContext());
1757	}
1758
1759	public boolean forceEncryption() {
1760		return getPreferences().getBoolean("force_encryption", false);
1761	}
1762
1763	public boolean confirmMessages() {
1764		return getPreferences().getBoolean("confirm_messages", true);
1765	}
1766
1767	public boolean saveEncryptedMessages() {
1768		return !getPreferences().getBoolean("dont_save_encrypted", false);
1769	}
1770
1771	public boolean indicateReceived() {
1772		return getPreferences().getBoolean("indicate_received", false);
1773	}
1774
1775	public void updateConversationUi() {
1776		if (mOnConversationUpdate != null) {
1777			mOnConversationUpdate.onConversationUpdate();
1778		}
1779	}
1780
1781	public void updateAccountUi() {
1782		if (mOnAccountUpdate != null) {
1783			mOnAccountUpdate.onAccountUpdate();
1784		}
1785	}
1786
1787	public void updateRosterUi() {
1788		if (mOnRosterUpdate != null) {
1789			mOnRosterUpdate.onRosterUpdate();
1790		}
1791	}
1792
1793	public Account findAccountByJid(final Jid accountJid) {
1794		for (Account account : this.accounts) {
1795			if (account.getJid().toBareJid().equals(accountJid)) {
1796				return account;
1797			}
1798		}
1799		return null;
1800	}
1801
1802	public Conversation findConversationByUuid(String uuid) {
1803		for (Conversation conversation : getConversations()) {
1804			if (conversation.getUuid().equals(uuid)) {
1805				return conversation;
1806			}
1807		}
1808		return null;
1809	}
1810
1811	public void markRead(Conversation conversation, boolean calledByUi) {
1812		mNotificationService.clear(conversation);
1813		String id = conversation.getLatestMarkableMessageId();
1814		conversation.markRead();
1815		if (confirmMessages() && id != null && calledByUi) {
1816			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1817					+ ": sending read marker for " + conversation.getName());
1818			Account account = conversation.getAccount();
1819			final Jid to = conversation.getContactJid();
1820			this.sendMessagePacket(conversation.getAccount(),
1821					mMessageGenerator.confirm(account, to, id));
1822		}
1823		if (!calledByUi) {
1824			updateConversationUi();
1825		}
1826	}
1827
1828	public void failWaitingOtrMessages(Conversation conversation) {
1829		for (Message message : conversation.getMessages()) {
1830			if (message.getEncryption() == Message.ENCRYPTION_OTR
1831					&& message.getStatus() == Message.STATUS_WAITING) {
1832				markMessage(message, Message.STATUS_SEND_FAILED);
1833			}
1834		}
1835	}
1836
1837	public SecureRandom getRNG() {
1838		return this.mRandom;
1839	}
1840
1841	public MemorizingTrustManager getMemorizingTrustManager() {
1842		return this.mMemorizingTrustManager;
1843	}
1844
1845	public PowerManager getPowerManager() {
1846		return this.pm;
1847	}
1848
1849	public LruCache<String, Bitmap> getBitmapCache() {
1850		return this.mBitmapCache;
1851	}
1852
1853	public void replyWithNotAcceptable(Account account, MessagePacket packet) {
1854		if (account.getStatus() == Account.STATUS_ONLINE) {
1855			MessagePacket error = this.mMessageGenerator
1856					.generateNotAcceptable(packet);
1857			sendMessagePacket(account, error);
1858		}
1859	}
1860
1861	public void syncRosterToDisk(final Account account) {
1862		new Thread(new Runnable() {
1863
1864			@Override
1865			public void run() {
1866				databaseBackend.writeRoster(account.getRoster());
1867			}
1868		}).start();
1869
1870	}
1871
1872	public List<String> getKnownHosts() {
1873		List<String> hosts = new ArrayList<>();
1874		for (Account account : getAccounts()) {
1875			if (!hosts.contains(account.getServer().toString())) {
1876				hosts.add(account.getServer().toString());
1877			}
1878			for (Contact contact : account.getRoster().getContacts()) {
1879				if (contact.showInRoster()) {
1880					final String server = contact.getServer().toString();
1881					if (server != null && !hosts.contains(server)) {
1882						hosts.add(server);
1883					}
1884				}
1885			}
1886		}
1887		return hosts;
1888	}
1889
1890	public List<String> getKnownConferenceHosts() {
1891		ArrayList<String> mucServers = new ArrayList<>();
1892		for (Account account : accounts) {
1893			if (account.getXmppConnection() != null) {
1894				String server = account.getXmppConnection().getMucServer();
1895				if (server != null && !mucServers.contains(server)) {
1896					mucServers.add(server);
1897				}
1898			}
1899		}
1900		return mucServers;
1901	}
1902
1903	public void sendMessagePacket(Account account, MessagePacket packet) {
1904		XmppConnection connection = account.getXmppConnection();
1905		if (connection != null) {
1906			connection.sendMessagePacket(packet);
1907		}
1908	}
1909
1910	public void sendPresencePacket(Account account, PresencePacket packet) {
1911		XmppConnection connection = account.getXmppConnection();
1912		if (connection != null) {
1913			connection.sendPresencePacket(packet);
1914		}
1915	}
1916
1917	public void sendIqPacket(Account account, IqPacket packet,
1918							 OnIqPacketReceived callback) {
1919		XmppConnection connection = account.getXmppConnection();
1920		if (connection != null) {
1921			connection.sendIqPacket(packet, callback);
1922		}
1923	}
1924
1925	public MessageGenerator getMessageGenerator() {
1926		return this.mMessageGenerator;
1927	}
1928
1929	public PresenceGenerator getPresenceGenerator() {
1930		return this.mPresenceGenerator;
1931	}
1932
1933	public IqGenerator getIqGenerator() {
1934		return this.mIqGenerator;
1935	}
1936
1937	public JingleConnectionManager getJingleConnectionManager() {
1938		return this.mJingleConnectionManager;
1939	}
1940
1941	public List<Contact> findContacts(String jid) {
1942		ArrayList<Contact> contacts = new ArrayList<>();
1943		for (Account account : getAccounts()) {
1944			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1945				Contact contact = account.getRoster().getContactFromRoster(jid);
1946				if (contact != null) {
1947					contacts.add(contact);
1948				}
1949			}
1950		}
1951		return contacts;
1952	}
1953
1954	public NotificationService getNotificationService() {
1955		return this.mNotificationService;
1956	}
1957
1958	public HttpConnectionManager getHttpConnectionManager() {
1959		return this.mHttpConnectionManager;
1960	}
1961
1962	public void resendFailedMessages(Message message) {
1963		List<Message> messages = new ArrayList<>();
1964		Message current = message;
1965		while (current.getStatus() == Message.STATUS_SEND_FAILED) {
1966			messages.add(current);
1967			if (current.mergeable(current.next())) {
1968				current = current.next();
1969			} else {
1970				break;
1971			}
1972		}
1973		for (Message msg : messages) {
1974			markMessage(msg, Message.STATUS_WAITING);
1975			this.resendMessage(msg);
1976		}
1977	}
1978
1979	public interface OnConversationUpdate {
1980		public void onConversationUpdate();
1981	}
1982
1983	public interface OnAccountUpdate {
1984		public void onAccountUpdate();
1985	}
1986
1987	public interface OnRosterUpdate {
1988		public void onRosterUpdate();
1989	}
1990
1991	public class XmppConnectionBinder extends Binder {
1992		public XmppConnectionService getService() {
1993			return XmppConnectionService.this;
1994		}
1995	}
1996
1997	private class DeletedDownloadable implements Downloadable {
1998
1999		@Override
2000		public boolean start() {
2001			return false;
2002		}
2003
2004		@Override
2005		public int getStatus() {
2006			return Downloadable.STATUS_DELETED;
2007		}
2008
2009		@Override
2010		public long getFileSize() {
2011			return 0;
2012		}
2013
2014		@Override
2015		public int getProgress() {
2016			return 0;
2017		}
2018
2019		@Override
2020		public String getMimeType() {
2021			return "";
2022		}
2023
2024	}
2025}