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