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.Looper;
  20import android.os.PowerManager;
  21import android.os.PowerManager.WakeLock;
  22import android.os.SystemClock;
  23import android.preference.PreferenceManager;
  24import android.provider.ContactsContract;
  25import android.util.Log;
  26import android.util.LruCache;
  27
  28import net.java.otr4j.OtrException;
  29import net.java.otr4j.session.Session;
  30import net.java.otr4j.session.SessionID;
  31import net.java.otr4j.session.SessionStatus;
  32
  33import org.openintents.openpgp.util.OpenPgpApi;
  34import org.openintents.openpgp.util.OpenPgpServiceConnection;
  35
  36import java.math.BigInteger;
  37import java.security.SecureRandom;
  38import java.util.ArrayList;
  39import java.util.Arrays;
  40import java.util.Collection;
  41import java.util.Collections;
  42import java.util.Comparator;
  43import java.util.Hashtable;
  44import java.util.Iterator;
  45import java.util.List;
  46import java.util.Locale;
  47import java.util.Map;
  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.Blockable;
  56import eu.siacs.conversations.entities.Bookmark;
  57import eu.siacs.conversations.entities.Contact;
  58import eu.siacs.conversations.entities.Conversation;
  59import eu.siacs.conversations.entities.Downloadable;
  60import eu.siacs.conversations.entities.DownloadablePlaceholder;
  61import eu.siacs.conversations.entities.Message;
  62import eu.siacs.conversations.entities.MucOptions;
  63import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
  64import eu.siacs.conversations.entities.Presences;
  65import eu.siacs.conversations.generator.IqGenerator;
  66import eu.siacs.conversations.generator.MessageGenerator;
  67import eu.siacs.conversations.generator.PresenceGenerator;
  68import eu.siacs.conversations.http.HttpConnectionManager;
  69import eu.siacs.conversations.parser.IqParser;
  70import eu.siacs.conversations.parser.MessageParser;
  71import eu.siacs.conversations.parser.PresenceParser;
  72import eu.siacs.conversations.persistance.DatabaseBackend;
  73import eu.siacs.conversations.persistance.FileBackend;
  74import eu.siacs.conversations.ui.UiCallback;
  75import eu.siacs.conversations.utils.CryptoHelper;
  76import eu.siacs.conversations.utils.ExceptionHelper;
  77import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
  78import eu.siacs.conversations.utils.PRNGFixes;
  79import eu.siacs.conversations.utils.PhoneHelper;
  80import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
  81import eu.siacs.conversations.utils.Xmlns;
  82import eu.siacs.conversations.xml.Element;
  83import eu.siacs.conversations.xmpp.OnBindListener;
  84import eu.siacs.conversations.xmpp.OnContactStatusChanged;
  85import eu.siacs.conversations.xmpp.OnIqPacketReceived;
  86import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
  87import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
  88import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
  89import eu.siacs.conversations.xmpp.OnStatusChanged;
  90import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
  91import eu.siacs.conversations.xmpp.XmppConnection;
  92import eu.siacs.conversations.xmpp.chatstate.ChatState;
  93import eu.siacs.conversations.xmpp.forms.Data;
  94import eu.siacs.conversations.xmpp.forms.Field;
  95import eu.siacs.conversations.xmpp.jid.InvalidJidException;
  96import eu.siacs.conversations.xmpp.jid.Jid;
  97import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
  98import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  99import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 100import eu.siacs.conversations.xmpp.pep.Avatar;
 101import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 102import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 103import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 104import me.leolin.shortcutbadger.ShortcutBadger;
 105
 106public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener {
 107
 108	public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification";
 109	public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground";
 110	private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
 111	public static final String ACTION_TRY_AGAIN = "try_again";
 112	public static final String ACTION_DISABLE_ACCOUNT = "disable_account";
 113	private ContentObserver contactObserver = new ContentObserver(null) {
 114		@Override
 115		public void onChange(boolean selfChange) {
 116			super.onChange(selfChange);
 117			Intent intent = new Intent(getApplicationContext(),
 118					XmppConnectionService.class);
 119			intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
 120			startService(intent);
 121		}
 122	};
 123
 124	private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor();
 125	private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor();
 126
 127	private final IBinder mBinder = new XmppConnectionBinder();
 128	private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
 129	private final FileObserver fileObserver = new FileObserver(
 130			FileBackend.getConversationsImageDirectory()) {
 131
 132		@Override
 133		public void onEvent(int event, String path) {
 134			if (event == FileObserver.DELETE) {
 135				markFileDeleted(path.split("\\.")[0]);
 136			}
 137		}
 138	};
 139	private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
 140
 141		@Override
 142		public void onJinglePacketReceived(Account account, JinglePacket packet) {
 143			mJingleConnectionManager.deliverPacket(account, packet);
 144		}
 145	};
 146	private final OnBindListener mOnBindListener = new OnBindListener() {
 147
 148		@Override
 149		public void onBind(final Account account) {
 150			account.getRoster().clearPresences();
 151			account.pendingConferenceJoins.clear();
 152			account.pendingConferenceLeaves.clear();
 153			fetchRosterFromServer(account);
 154			fetchBookmarks(account);
 155			sendPresence(account);
 156			connectMultiModeConversations(account);
 157			updateConversationUi();
 158		}
 159	};
 160	private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
 161
 162		@Override
 163		public void onMessageAcknowledged(Account account, String uuid) {
 164			for (final Conversation conversation : getConversations()) {
 165				if (conversation.getAccount() == account) {
 166					Message message = conversation.findUnsentMessageWithUuid(uuid);
 167					if (message != null) {
 168						markMessage(message, Message.STATUS_SEND);
 169						if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) {
 170							databaseBackend.updateConversation(conversation);
 171						}
 172					}
 173				}
 174			}
 175		}
 176	};
 177	private final IqGenerator mIqGenerator = new IqGenerator(this);
 178	public DatabaseBackend databaseBackend;
 179	public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
 180
 181		@Override
 182		public void onContactStatusChanged(Contact contact, boolean online) {
 183			Conversation conversation = find(getConversations(), contact);
 184			if (conversation != null) {
 185				if (online) {
 186					conversation.endOtrIfNeeded();
 187					if (contact.getPresences().size() == 1) {
 188						sendUnsentMessages(conversation);
 189					}
 190				} else {
 191					if (contact.getPresences().size() >= 1) {
 192						if (conversation.hasValidOtrSession()) {
 193							String otrResource = conversation.getOtrSession().getSessionID().getUserID();
 194							if (!(Arrays.asList(contact.getPresences().asStringArray()).contains(otrResource))) {
 195								conversation.endOtrIfNeeded();
 196							}
 197						}
 198					} else {
 199						conversation.endOtrIfNeeded();
 200					}
 201				}
 202			}
 203		}
 204	};
 205	private FileBackend fileBackend = new FileBackend(this);
 206	private MemorizingTrustManager mMemorizingTrustManager;
 207	private NotificationService mNotificationService = new NotificationService(
 208			this);
 209	private OnMessagePacketReceived mMessageParser = new MessageParser(this);
 210	private OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
 211	private IqParser mIqParser = new IqParser(this);
 212	private OnIqPacketReceived mDefaultIqHandler = new OnIqPacketReceived() {
 213		@Override
 214		public void onIqPacketReceived(Account account, IqPacket packet) {
 215			if (packet.getType() == IqPacket.TYPE.ERROR) {
 216				Element error = packet.findChild("error");
 217				String text = error != null ? error.findChildContent("text") : null;
 218				if (text != null) {
 219					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received iq error - "+text);
 220				}
 221			}
 222		}
 223	};
 224	private MessageGenerator mMessageGenerator = new MessageGenerator(this);
 225	private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
 226	private List<Account> accounts;
 227	private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
 228			this);
 229	private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
 230			this);
 231	private AvatarService mAvatarService = new AvatarService(this);
 232	private final List<String> mInProgressAvatarFetches = new ArrayList<>();
 233	private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
 234	private OnConversationUpdate mOnConversationUpdate = null;
 235	private int convChangedListenerCount = 0;
 236	private int unreadCount = 0;
 237	private OnAccountUpdate mOnAccountUpdate = null;
 238	private OnStatusChanged statusListener = new OnStatusChanged() {
 239
 240		@Override
 241		public void onStatusChanged(Account account) {
 242			XmppConnection connection = account.getXmppConnection();
 243			if (mOnAccountUpdate != null) {
 244				mOnAccountUpdate.onAccountUpdate();
 245			}
 246			if (account.getStatus() == Account.State.ONLINE) {
 247				for (Conversation conversation : account.pendingConferenceLeaves) {
 248					leaveMuc(conversation);
 249				}
 250				for (Conversation conversation : account.pendingConferenceJoins) {
 251					joinMuc(conversation);
 252				}
 253				mMessageArchiveService.executePendingQueries(account);
 254				mJingleConnectionManager.cancelInTransmission();
 255				List<Conversation> conversations = getConversations();
 256				for (Conversation conversation : conversations) {
 257					if (conversation.getAccount() == account) {
 258						conversation.startOtrIfNeeded();
 259						sendUnsentMessages(conversation);
 260					}
 261				}
 262				if (connection != null && connection.getFeatures().csi()) {
 263					if (checkListeners()) {
 264						Log.d(Config.LOGTAG, account.getJid().toBareJid()
 265								+ " sending csi//inactive");
 266						connection.sendInactive();
 267					} else {
 268						Log.d(Config.LOGTAG, account.getJid().toBareJid()
 269								+ " sending csi//active");
 270						connection.sendActive();
 271					}
 272				}
 273				syncDirtyContacts(account);
 274				scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode());
 275			} else if (account.getStatus() == Account.State.OFFLINE) {
 276				resetSendingToWaiting(account);
 277				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 278					int timeToReconnect = mRandom.nextInt(50) + 10;
 279					scheduleWakeUpCall(timeToReconnect,account.getUuid().hashCode());
 280				}
 281			} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
 282				databaseBackend.updateAccount(account);
 283				reconnectAccount(account, true);
 284			} else if ((account.getStatus() != Account.State.CONNECTING)
 285					&& (account.getStatus() != Account.State.NO_INTERNET)) {
 286				if (connection != null) {
 287					int next = connection.getTimeToNextAttempt();
 288					Log.d(Config.LOGTAG, account.getJid().toBareJid()
 289							+ ": error connecting account. try again in "
 290							+ next + "s for the "
 291							+ (connection.getAttempt() + 1) + " time");
 292					scheduleWakeUpCall(next,account.getUuid().hashCode());
 293				}
 294					}
 295			getNotificationService().updateErrorNotification();
 296		}
 297	};
 298	private int accountChangedListenerCount = 0;
 299	private OnRosterUpdate mOnRosterUpdate = null;
 300	private OnUpdateBlocklist mOnUpdateBlocklist = null;
 301	private int updateBlocklistListenerCount = 0;
 302	private int rosterChangedListenerCount = 0;
 303	private OnMucRosterUpdate mOnMucRosterUpdate = null;
 304	private int mucRosterChangedListenerCount = 0;
 305	private SecureRandom mRandom;
 306	private OpenPgpServiceConnection pgpServiceConnection;
 307	private PgpEngine mPgpEngine = null;
 308	private WakeLock wakeLock;
 309	private PowerManager pm;
 310	private LruCache<String, Bitmap> mBitmapCache;
 311	private Thread mPhoneContactMergerThread;
 312
 313	private boolean mRestoredFromDatabase = false;
 314	public boolean areMessagesInitialized() {
 315		return this.mRestoredFromDatabase;
 316	}
 317
 318	public PgpEngine getPgpEngine() {
 319		if (pgpServiceConnection.isBound()) {
 320			if (this.mPgpEngine == null) {
 321				this.mPgpEngine = new PgpEngine(new OpenPgpApi(
 322							getApplicationContext(),
 323							pgpServiceConnection.getService()), this);
 324			}
 325			return mPgpEngine;
 326		} else {
 327			return null;
 328		}
 329
 330	}
 331
 332	public FileBackend getFileBackend() {
 333		return this.fileBackend;
 334	}
 335
 336	public AvatarService getAvatarService() {
 337		return this.mAvatarService;
 338	}
 339
 340	public void attachLocationToConversation(final Conversation conversation,
 341											 final Uri uri,
 342											 final UiCallback<Message> callback) {
 343		int encryption = conversation.getNextEncryption(forceEncryption());
 344		if (encryption == Message.ENCRYPTION_PGP) {
 345			encryption = Message.ENCRYPTION_DECRYPTED;
 346		}
 347		Message message = new Message(conversation,uri.toString(),encryption);
 348		if (conversation.getNextCounterpart() != null) {
 349			message.setCounterpart(conversation.getNextCounterpart());
 350		}
 351		if (encryption == Message.ENCRYPTION_DECRYPTED) {
 352			getPgpEngine().encrypt(message, callback);
 353		} else {
 354			callback.success(message);
 355		}
 356	}
 357
 358	public void attachFileToConversation(final Conversation conversation,
 359			final Uri uri,
 360			final UiCallback<Message> callback) {
 361		final Message message;
 362		if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 363			message = new Message(conversation, "",
 364					Message.ENCRYPTION_DECRYPTED);
 365		} else {
 366			message = new Message(conversation, "",
 367					conversation.getNextEncryption(forceEncryption()));
 368		}
 369		message.setCounterpart(conversation.getNextCounterpart());
 370		message.setType(Message.TYPE_FILE);
 371		String path = getFileBackend().getOriginalPath(uri);
 372		if (path!=null) {
 373			message.setRelativeFilePath(path);
 374			getFileBackend().updateFileParams(message);
 375			if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 376				getPgpEngine().encrypt(message, callback);
 377			} else {
 378				callback.success(message);
 379			}
 380		} else {
 381			mFileAddingExecutor.execute(new Runnable() {
 382				@Override
 383				public void run() {
 384					try {
 385						getFileBackend().copyFileToPrivateStorage(message, uri);
 386						getFileBackend().updateFileParams(message);
 387						if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 388							getPgpEngine().encrypt(message, callback);
 389						} else {
 390							callback.success(message);
 391						}
 392					} catch (FileBackend.FileCopyException e) {
 393						callback.error(e.getResId(), message);
 394					}
 395				}
 396			});
 397		}
 398	}
 399
 400	public void attachImageToConversation(final Conversation conversation,
 401			final Uri uri, final UiCallback<Message> callback) {
 402		final Message message;
 403		if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 404			message = new Message(conversation, "",
 405					Message.ENCRYPTION_DECRYPTED);
 406		} else {
 407			message = new Message(conversation, "",
 408					conversation.getNextEncryption(forceEncryption()));
 409		}
 410		message.setCounterpart(conversation.getNextCounterpart());
 411		message.setType(Message.TYPE_IMAGE);
 412		mFileAddingExecutor.execute(new Runnable() {
 413
 414			@Override
 415			public void run() {
 416				try {
 417					getFileBackend().copyImageToPrivateStorage(message, uri);
 418					if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 419						getPgpEngine().encrypt(message, callback);
 420					} else {
 421						callback.success(message);
 422					}
 423				} catch (final FileBackend.FileCopyException e) {
 424					callback.error(e.getResId(), message);
 425				}
 426			}
 427		});
 428	}
 429
 430	public Conversation find(Bookmark bookmark) {
 431		return find(bookmark.getAccount(), bookmark.getJid());
 432	}
 433
 434	public Conversation find(final Account account, final Jid jid) {
 435		return find(getConversations(), account, jid);
 436	}
 437
 438	@Override
 439	public int onStartCommand(Intent intent, int flags, int startId) {
 440		final String action = intent == null ? null : intent.getAction();
 441		if (action != null) {
 442			switch (action) {
 443				case ConnectivityManager.CONNECTIVITY_ACTION:
 444					if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
 445						resetAllAttemptCounts(true);
 446					}
 447					break;
 448				case ACTION_MERGE_PHONE_CONTACTS:
 449					if (mRestoredFromDatabase) {
 450						PhoneHelper.loadPhoneContacts(getApplicationContext(),
 451								new CopyOnWriteArrayList<Bundle>(),
 452								this);
 453					}
 454					return START_STICKY;
 455				case Intent.ACTION_SHUTDOWN:
 456					logoutAndSave();
 457					return START_NOT_STICKY;
 458				case ACTION_CLEAR_NOTIFICATION:
 459					mNotificationService.clear();
 460					break;
 461				case ACTION_DISABLE_FOREGROUND:
 462					getPreferences().edit().putBoolean("keep_foreground_service",false).commit();
 463					toggleForegroundService();
 464					break;
 465				case ACTION_TRY_AGAIN:
 466					resetAllAttemptCounts(false);
 467					break;
 468				case ACTION_DISABLE_ACCOUNT:
 469					try {
 470						String jid = intent.getStringExtra("account");
 471						Account account = jid == null ? null : findAccountByJid(Jid.fromString(jid));
 472						if (account != null) {
 473							account.setOption(Account.OPTION_DISABLED,true);
 474							updateAccount(account);
 475						}
 476					} catch (final InvalidJidException ignored) {
 477						break;
 478					}
 479					break;
 480			}
 481		}
 482		this.wakeLock.acquire();
 483
 484		for (Account account : accounts) {
 485			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 486				if (!hasInternetConnection()) {
 487					account.setStatus(Account.State.NO_INTERNET);
 488					if (statusListener != null) {
 489						statusListener.onStatusChanged(account);
 490					}
 491				} else {
 492					if (account.getStatus() == Account.State.NO_INTERNET) {
 493						account.setStatus(Account.State.OFFLINE);
 494						if (statusListener != null) {
 495							statusListener.onStatusChanged(account);
 496						}
 497					}
 498					if (account.getStatus() == Account.State.ONLINE) {
 499						long lastReceived = account.getXmppConnection().getLastPacketReceived();
 500						long lastSent = account.getXmppConnection().getLastPingSent();
 501						long pingInterval = "ui".equals(action) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
 502						long msToNextPing = (Math.max(lastReceived,lastSent) + pingInterval) - SystemClock.elapsedRealtime();
 503						long pingTimeoutIn = (lastSent + Config.PING_TIMEOUT * 1000) - SystemClock.elapsedRealtime();
 504						if (lastSent > lastReceived) {
 505							if (pingTimeoutIn < 0) {
 506								Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout");
 507								this.reconnectAccount(account, true);
 508							} else {
 509								int secs = (int) (pingTimeoutIn / 1000);
 510								this.scheduleWakeUpCall(secs,account.getUuid().hashCode());
 511							}
 512						} else if (msToNextPing <= 0) {
 513							account.getXmppConnection().sendPing();
 514							Log.d(Config.LOGTAG, account.getJid().toBareJid()+" send ping");
 515							this.scheduleWakeUpCall(Config.PING_TIMEOUT,account.getUuid().hashCode());
 516						} else {
 517							this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
 518						}
 519					} else if (account.getStatus() == Account.State.OFFLINE) {
 520						reconnectAccount(account,true);
 521					} else if (account.getStatus() == Account.State.CONNECTING) {
 522						long timeout = Config.CONNECT_TIMEOUT - ((SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000);
 523						if (timeout < 0) {
 524							Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting");
 525							reconnectAccount(account, true);
 526						} else {
 527							scheduleWakeUpCall((int) timeout,account.getUuid().hashCode());
 528						}
 529					} else {
 530						if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
 531							reconnectAccount(account, true);
 532						}
 533					}
 534
 535				}
 536				if (mOnAccountUpdate != null) {
 537					mOnAccountUpdate.onAccountUpdate();
 538				}
 539			}
 540		}
 541		/*PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE);
 542			if (!pm.isScreenOn()) {
 543			removeStaleListeners();
 544			}*/
 545		if (wakeLock.isHeld()) {
 546			try {
 547				wakeLock.release();
 548			} catch (final RuntimeException ignored) {
 549			}
 550		}
 551		return START_STICKY;
 552	}
 553
 554	private void resetAllAttemptCounts(boolean reallyAll) {
 555		Log.d(Config.LOGTAG,"resetting all attepmt counts");
 556		for(Account account : accounts) {
 557			if (account.hasErrorStatus() || reallyAll) {
 558				final XmppConnection connection = account.getXmppConnection();
 559				if (connection != null) {
 560					connection.resetAttemptCount();
 561				}
 562			}
 563		}
 564	}
 565
 566	public boolean hasInternetConnection() {
 567		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
 568			.getSystemService(Context.CONNECTIVITY_SERVICE);
 569		NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
 570		return activeNetwork != null && activeNetwork.isConnected();
 571	}
 572
 573	@SuppressLint("TrulyRandom")
 574	@Override
 575	public void onCreate() {
 576		ExceptionHelper.init(getApplicationContext());
 577		PRNGFixes.apply();
 578		this.mRandom = new SecureRandom();
 579		updateMemorizingTrustmanager();
 580		final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
 581		final int cacheSize = maxMemory / 8;
 582		this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
 583			@Override
 584			protected int sizeOf(final String key, final Bitmap bitmap) {
 585				return bitmap.getByteCount() / 1024;
 586			}
 587		};
 588
 589		this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
 590		this.accounts = databaseBackend.getAccounts();
 591
 592		for (final Account account : this.accounts) {
 593			account.initAccountServices(this);
 594		}
 595		restoreFromDatabase();
 596
 597		getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
 598		this.fileObserver.startWatching();
 599		this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain");
 600		this.pgpServiceConnection.bindToService();
 601
 602		this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 603		this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"XmppConnectionService");
 604		toggleForegroundService();
 605	}
 606
 607	public void toggleForegroundService() {
 608		if (getPreferences().getBoolean("keep_foreground_service",false)) {
 609			startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
 610		} else {
 611			stopForeground(true);
 612		}
 613	}
 614
 615	@Override
 616	public void onTaskRemoved(final Intent rootIntent) {
 617		super.onTaskRemoved(rootIntent);
 618		if (!getPreferences().getBoolean("keep_foreground_service",false)) {
 619			this.logoutAndSave();
 620		}
 621	}
 622
 623	private void logoutAndSave() {
 624		for (final Account account : accounts) {
 625			databaseBackend.writeRoster(account.getRoster());
 626			if (account.getXmppConnection() != null) {
 627				disconnect(account, false);
 628			}
 629		}
 630		Context context = getApplicationContext();
 631		AlarmManager alarmManager = (AlarmManager) context
 632			.getSystemService(Context.ALARM_SERVICE);
 633		Intent intent = new Intent(context, EventReceiver.class);
 634		alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
 635		Log.d(Config.LOGTAG, "good bye");
 636		stopSelf();
 637	}
 638
 639	protected void scheduleWakeUpCall(int seconds, int requestCode) {
 640		final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000;
 641
 642		Context context = getApplicationContext();
 643		AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
 644
 645		Intent intent = new Intent(context, EventReceiver.class);
 646		intent.setAction("ping");
 647		PendingIntent alarmIntent = PendingIntent.getBroadcast(context, requestCode, intent, 0);
 648		alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, alarmIntent);
 649	}
 650
 651	public XmppConnection createConnection(final Account account) {
 652		final SharedPreferences sharedPref = getPreferences();
 653		account.setResource(sharedPref.getString("resource", "mobile")
 654				.toLowerCase(Locale.getDefault()));
 655		final XmppConnection connection = new XmppConnection(account, this);
 656		connection.setOnMessagePacketReceivedListener(this.mMessageParser);
 657		connection.setOnStatusChangedListener(this.statusListener);
 658		connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
 659		connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
 660		connection.setOnJinglePacketReceivedListener(this.jingleListener);
 661		connection.setOnBindListener(this.mOnBindListener);
 662		connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
 663		connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
 664		return connection;
 665	}
 666
 667	public void sendChatState(Conversation conversation) {
 668		if (sendChatStates()) {
 669			MessagePacket packet = mMessageGenerator.generateChatState(conversation);
 670			sendMessagePacket(conversation.getAccount(), packet);
 671		}
 672	}
 673
 674	private void sendFileMessage(final Message message) {
 675		Log.d(Config.LOGTAG, "send file message");
 676		final Account account = message.getConversation().getAccount();
 677		final XmppConnection connection = account.getXmppConnection();
 678		if (connection != null && connection.getFeatures().httpUpload()) {
 679			mHttpConnectionManager.createNewUploadConnection(message);
 680		} else {
 681			mJingleConnectionManager.createNewConnection(message);
 682		}
 683	}
 684
 685	public void sendMessage(final Message message) {
 686		final Account account = message.getConversation().getAccount();
 687		account.deactivateGracePeriod();
 688		final Conversation conv = message.getConversation();
 689		MessagePacket packet = null;
 690		boolean saveInDb = true;
 691		boolean send = false;
 692		if (account.getStatus() == Account.State.ONLINE
 693				&& account.getXmppConnection() != null) {
 694			if (message.needsUploading()) {
 695				if (message.getCounterpart() != null || account.httpUploadAvailable()) {
 696					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 697						if (!conv.hasValidOtrSession()) {
 698							conv.startOtrSession(message.getCounterpart().getResourcepart(),true);
 699							message.setStatus(Message.STATUS_WAITING);
 700						} else if (conv.hasValidOtrSession()
 701								&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 702							mJingleConnectionManager.createNewConnection(message);
 703						}
 704					} else {
 705						this.sendFileMessage(message);
 706
 707					}
 708				} else {
 709					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 710						conv.startOtrIfNeeded();
 711					}
 712					message.setStatus(Message.STATUS_WAITING);
 713				}
 714			} else {
 715				if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 716					if (!conv.hasValidOtrSession() && (message.getCounterpart() != null)) {
 717						conv.startOtrSession(message.getCounterpart().getResourcepart(), true);
 718						message.setStatus(Message.STATUS_WAITING);
 719					} else if (conv.hasValidOtrSession()) {
 720						if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 721							packet = mMessageGenerator.generateOtrChat(message);
 722							send = true;
 723						} else {
 724							message.setStatus(Message.STATUS_WAITING);
 725							conv.startOtrIfNeeded();
 726						}
 727					} else {
 728						message.setStatus(Message.STATUS_WAITING);
 729					}
 730				} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 731					message.getConversation().endOtrIfNeeded();
 732					message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
 733						@Override
 734						public void onMessageFound(Message message) {
 735							markMessage(message,Message.STATUS_SEND_FAILED);
 736						}
 737					});
 738					packet = mMessageGenerator.generatePgpChat(message);
 739					send = true;
 740				} else {
 741					message.getConversation().endOtrIfNeeded();
 742					message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
 743						@Override
 744						public void onMessageFound(Message message) {
 745							markMessage(message,Message.STATUS_SEND_FAILED);
 746						}
 747					});
 748					packet = mMessageGenerator.generateChat(message);
 749					send = true;
 750				}
 751			}
 752			if (!account.getXmppConnection().getFeatures().sm()
 753					&& conv.getMode() != Conversation.MODE_MULTI) {
 754				message.setStatus(Message.STATUS_SEND);
 755					}
 756		} else {
 757			message.setStatus(Message.STATUS_WAITING);
 758			if (message.getType() == Message.TYPE_TEXT) {
 759				if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 760					String pgpBody = message.getEncryptedBody();
 761					String decryptedBody = message.getBody();
 762					message.setBody(pgpBody);
 763					message.setEncryption(Message.ENCRYPTION_PGP);
 764					databaseBackend.createMessage(message);
 765					saveInDb = false;
 766					message.setBody(decryptedBody);
 767					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 768				} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 769					if (!conv.hasValidOtrSession()
 770							&& message.getCounterpart() != null) {
 771						conv.startOtrSession(message.getCounterpart().getResourcepart(), false);
 772							}
 773				}
 774			}
 775
 776		}
 777		conv.add(message);
 778		if (saveInDb) {
 779			if (message.getEncryption() == Message.ENCRYPTION_NONE
 780					|| saveEncryptedMessages()) {
 781				databaseBackend.createMessage(message);
 782					}
 783		}
 784		if ((send) && (packet != null)) {
 785			if (conv.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
 786				if (this.sendChatStates()) {
 787					packet.addChild(ChatState.toElement(conv.getOutgoingChatState()));
 788				}
 789			}
 790			sendMessagePacket(account, packet);
 791		}
 792		updateConversationUi();
 793	}
 794
 795	private void sendUnsentMessages(final Conversation conversation) {
 796		conversation.findWaitingMessages(new Conversation.OnMessageFound() {
 797
 798			@Override
 799			public void onMessageFound(Message message) {
 800				resendMessage(message);
 801			}
 802		});
 803	}
 804
 805	public void resendMessage(final Message message) {
 806		Account account = message.getConversation().getAccount();
 807		MessagePacket packet = null;
 808		if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 809			Presences presences = message.getConversation().getContact().getPresences();
 810			if (!message.getConversation().hasValidOtrSession()) {
 811				if ((message.getCounterpart() != null)
 812						&& (presences.has(message.getCounterpart().getResourcepart()))) {
 813					message.getConversation().startOtrSession(message.getCounterpart().getResourcepart(), true);
 814				} else {
 815					if (presences.size() == 1) {
 816						String presence = presences.asStringArray()[0];
 817						message.getConversation().startOtrSession(presence, true);
 818					}
 819				}
 820			} else {
 821				if (message.getConversation().getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 822					try {
 823						message.setCounterpart(Jid.fromSessionID(message.getConversation().getOtrSession().getSessionID()));
 824						if (message.needsUploading()) {
 825							mJingleConnectionManager.createNewConnection(message);
 826						} else {
 827							packet = mMessageGenerator.generateOtrChat(message, true);
 828						}
 829					} catch (final InvalidJidException ignored) {
 830
 831					}
 832				}
 833			}
 834		} else if (message.needsUploading()) {
 835			Contact contact = message.getConversation().getContact();
 836			Presences presences = contact.getPresences();
 837			if (account.httpUploadAvailable() || (message.getCounterpart() != null && presences.has(message.getCounterpart().getResourcepart()))) {
 838				this.sendFileMessage(message);
 839			} else {
 840				if (presences.size() == 1) {
 841					String presence = presences.asStringArray()[0];
 842					try {
 843						message.setCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence));
 844					} catch (InvalidJidException e) {
 845						return;
 846					}
 847					this.sendFileMessage(message);
 848				}
 849			}
 850		} else {
 851			if (message.getEncryption() == Message.ENCRYPTION_NONE) {
 852				packet = mMessageGenerator.generateChat(message, true);
 853			} else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
 854					|| (message.getEncryption() == Message.ENCRYPTION_PGP)) {
 855				packet = mMessageGenerator.generatePgpChat(message, true);
 856			}
 857		}
 858		if (packet != null) {
 859			if (!account.getXmppConnection().getFeatures().sm()
 860					&& message.getConversation().getMode() != Conversation.MODE_MULTI) {
 861				markMessage(message, Message.STATUS_SEND);
 862			} else {
 863				markMessage(message, Message.STATUS_UNSEND);
 864			}
 865			if (message.getConversation().setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
 866				if (this.sendChatStates()) {
 867					packet.addChild(ChatState.toElement(message.getConversation().getOutgoingChatState()));
 868				}
 869			}
 870			sendMessagePacket(account, packet);
 871		}
 872	}
 873
 874	public void fetchRosterFromServer(final Account account) {
 875		final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
 876		if (!"".equals(account.getRosterVersion())) {
 877			Log.d(Config.LOGTAG, account.getJid().toBareJid()
 878					+ ": fetching roster version " + account.getRosterVersion());
 879		} else {
 880			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
 881		}
 882		iqPacket.query(Xmlns.ROSTER).setAttribute("ver",
 883				account.getRosterVersion());
 884		account.getXmppConnection().sendIqPacket(iqPacket, mIqParser);
 885	}
 886
 887	public void fetchBookmarks(final Account account) {
 888		final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
 889		final Element query = iqPacket.query("jabber:iq:private");
 890		query.addChild("storage", "storage:bookmarks");
 891		final OnIqPacketReceived callback = new OnIqPacketReceived() {
 892
 893			@Override
 894			public void onIqPacketReceived(final Account account, final IqPacket packet) {
 895				final Element query = packet.query();
 896				final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
 897				final Element storage = query.findChild("storage",
 898						"storage:bookmarks");
 899				if (storage != null) {
 900					for (final Element item : storage.getChildren()) {
 901						if (item.getName().equals("conference")) {
 902							final Bookmark bookmark = Bookmark.parse(item, account);
 903							bookmarks.add(bookmark);
 904							Conversation conversation = find(bookmark);
 905							if (conversation != null) {
 906								conversation.setBookmark(bookmark);
 907							} else if (bookmark.autojoin() && bookmark.getJid() != null) {
 908								conversation = findOrCreateConversation(
 909										account, bookmark.getJid(), true);
 910								conversation.setBookmark(bookmark);
 911								joinMuc(conversation);
 912							}
 913						}
 914					}
 915				}
 916				account.setBookmarks(bookmarks);
 917			}
 918		};
 919		sendIqPacket(account, iqPacket, callback);
 920	}
 921
 922	public void pushBookmarks(Account account) {
 923		IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
 924		Element query = iqPacket.query("jabber:iq:private");
 925		Element storage = query.addChild("storage", "storage:bookmarks");
 926		for (Bookmark bookmark : account.getBookmarks()) {
 927			storage.addChild(bookmark);
 928		}
 929		sendIqPacket(account, iqPacket, mDefaultIqHandler);
 930	}
 931
 932	public void onPhoneContactsLoaded(final List<Bundle> phoneContacts) {
 933		if (mPhoneContactMergerThread != null) {
 934			mPhoneContactMergerThread.interrupt();
 935		}
 936		mPhoneContactMergerThread = new Thread(new Runnable() {
 937			@Override
 938			public void run() {
 939				Log.d(Config.LOGTAG,"start merging phone contacts with roster");
 940				for (Account account : accounts) {
 941					List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts();
 942					for (Bundle phoneContact : phoneContacts) {
 943						if (Thread.interrupted()) {
 944							Log.d(Config.LOGTAG,"interrupted merging phone contacts");
 945							return;
 946						}
 947						Jid jid;
 948						try {
 949							jid = Jid.fromString(phoneContact.getString("jid"));
 950						} catch (final InvalidJidException e) {
 951							continue;
 952						}
 953						final Contact contact = account.getRoster().getContact(jid);
 954						String systemAccount = phoneContact.getInt("phoneid")
 955							+ "#"
 956							+ phoneContact.getString("lookup");
 957						contact.setSystemAccount(systemAccount);
 958						if (contact.setPhotoUri(phoneContact.getString("photouri"))) {
 959							getAvatarService().clear(contact);
 960						}
 961						contact.setSystemName(phoneContact.getString("displayname"));
 962						withSystemAccounts.remove(contact);
 963					}
 964					for(Contact contact : withSystemAccounts) {
 965						contact.setSystemAccount(null);
 966						contact.setSystemName(null);
 967						if (contact.setPhotoUri(null)) {
 968							getAvatarService().clear(contact);
 969						}
 970					}
 971				}
 972				Log.d(Config.LOGTAG,"finished merging phone contacts");
 973				updateAccountUi();
 974			}
 975		});
 976		mPhoneContactMergerThread.start();
 977	}
 978
 979	private void restoreFromDatabase() {
 980		synchronized (this.conversations) {
 981			final Map<String, Account> accountLookupTable = new Hashtable<>();
 982			for (Account account : this.accounts) {
 983				accountLookupTable.put(account.getUuid(), account);
 984			}
 985			this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
 986			for (Conversation conversation : this.conversations) {
 987				Account account = accountLookupTable.get(conversation.getAccountUuid());
 988				conversation.setAccount(account);
 989			}
 990			Runnable runnable =new Runnable() {
 991				@Override
 992				public void run() {
 993					Log.d(Config.LOGTAG,"restoring roster");
 994					for(Account account : accounts) {
 995						databaseBackend.readRoster(account.getRoster());
 996					}
 997					getBitmapCache().evictAll();
 998					Looper.prepare();
 999					PhoneHelper.loadPhoneContacts(getApplicationContext(),
1000							new CopyOnWriteArrayList<Bundle>(),
1001							XmppConnectionService.this);
1002					Log.d(Config.LOGTAG,"restoring messages");
1003					for (Conversation conversation : conversations) {
1004						conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1005						checkDeletedFiles(conversation);
1006					}
1007					mRestoredFromDatabase = true;
1008					Log.d(Config.LOGTAG,"restored all messages");
1009					updateConversationUi();
1010				}
1011			};
1012			mDatabaseExecutor.execute(runnable);
1013		}
1014	}
1015
1016	public List<Conversation> getConversations() {
1017		return this.conversations;
1018	}
1019
1020	private void checkDeletedFiles(Conversation conversation) {
1021		conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
1022
1023			@Override
1024			public void onMessageFound(Message message) {
1025				if (!getFileBackend().isFileAvailable(message)) {
1026					message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
1027				}
1028			}
1029		});
1030	}
1031
1032	private void markFileDeleted(String uuid) {
1033		for (Conversation conversation : getConversations()) {
1034			Message message = conversation.findMessageWithFileAndUuid(uuid);
1035			if (message != null) {
1036				if (!getFileBackend().isFileAvailable(message)) {
1037					message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
1038					updateConversationUi();
1039				}
1040				return;
1041			}
1042		}
1043	}
1044
1045	public void populateWithOrderedConversations(final List<Conversation> list) {
1046		populateWithOrderedConversations(list, true);
1047	}
1048
1049	public void populateWithOrderedConversations(final List<Conversation> list, boolean includeConferences) {
1050		list.clear();
1051		if (includeConferences) {
1052			list.addAll(getConversations());
1053		} else {
1054			for (Conversation conversation : getConversations()) {
1055				if (conversation.getMode() == Conversation.MODE_SINGLE) {
1056					list.add(conversation);
1057				}
1058			}
1059		}
1060		Collections.sort(list, new Comparator<Conversation>() {
1061			@Override
1062			public int compare(Conversation lhs, Conversation rhs) {
1063				Message left = lhs.getLatestMessage();
1064				Message right = rhs.getLatestMessage();
1065				if (left.getTimeSent() > right.getTimeSent()) {
1066					return -1;
1067				} else if (left.getTimeSent() < right.getTimeSent()) {
1068					return 1;
1069				} else {
1070					return 0;
1071				}
1072			}
1073		});
1074	}
1075
1076	public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
1077		Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp));
1078		if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) {
1079			return;
1080		}
1081		Runnable runnable = new Runnable() {
1082			@Override
1083			public void run() {
1084				final Account account = conversation.getAccount();
1085				List<Message> messages = databaseBackend.getMessages(conversation, 50,timestamp);
1086				if (messages.size() > 0) {
1087					conversation.addAll(0, messages);
1088					checkDeletedFiles(conversation);
1089					callback.onMoreMessagesLoaded(messages.size(), conversation);
1090				} else if (conversation.hasMessagesLeftOnServer()
1091						&& account.isOnlineAndConnected()
1092						&& account.getXmppConnection().getFeatures().mam()) {
1093					MessageArchiveService.Query query = getMessageArchiveService().query(conversation,0,timestamp - 1);
1094					if (query != null) {
1095						query.setCallback(callback);
1096					}
1097					callback.informUser(R.string.fetching_history_from_server);
1098				}
1099			}
1100		};
1101		mDatabaseExecutor.execute(runnable);
1102	}
1103
1104	public List<Account> getAccounts() {
1105		return this.accounts;
1106	}
1107
1108	public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
1109		for (final Conversation conversation : haystack) {
1110			if (conversation.getContact() == contact) {
1111				return conversation;
1112			}
1113		}
1114		return null;
1115	}
1116
1117	public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
1118		if (jid == null) {
1119			return null;
1120		}
1121		for (final Conversation conversation : haystack) {
1122			if ((account == null || conversation.getAccount() == account)
1123					&& (conversation.getJid().toBareJid().equals(jid.toBareJid()))) {
1124				return conversation;
1125			}
1126		}
1127		return null;
1128	}
1129
1130	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) {
1131		return this.findOrCreateConversation(account, jid, muc, null);
1132	}
1133
1134	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) {
1135		synchronized (this.conversations) {
1136			Conversation conversation = find(account, jid);
1137			if (conversation != null) {
1138				return conversation;
1139			}
1140			conversation = databaseBackend.findConversation(account, jid);
1141			if (conversation != null) {
1142				conversation.setStatus(Conversation.STATUS_AVAILABLE);
1143				conversation.setAccount(account);
1144				if (muc) {
1145					conversation.setMode(Conversation.MODE_MULTI);
1146					conversation.setContactJid(jid);
1147				} else {
1148					conversation.setMode(Conversation.MODE_SINGLE);
1149					conversation.setContactJid(jid.toBareJid());
1150				}
1151				conversation.setNextEncryption(-1);
1152				conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1153				this.databaseBackend.updateConversation(conversation);
1154			} else {
1155				String conversationName;
1156				Contact contact = account.getRoster().getContact(jid);
1157				if (contact != null) {
1158					conversationName = contact.getDisplayName();
1159				} else {
1160					conversationName = jid.getLocalpart();
1161				}
1162				if (muc) {
1163					conversation = new Conversation(conversationName, account, jid,
1164							Conversation.MODE_MULTI);
1165				} else {
1166					conversation = new Conversation(conversationName, account, jid.toBareJid(),
1167							Conversation.MODE_SINGLE);
1168				}
1169				this.databaseBackend.createConversation(conversation);
1170			}
1171			if (account.getXmppConnection() != null
1172					&& account.getXmppConnection().getFeatures().mam()
1173					&& !muc) {
1174				if (query == null) {
1175					this.mMessageArchiveService.query(conversation);
1176				} else {
1177					if (query.getConversation() == null) {
1178						this.mMessageArchiveService.query(conversation, query.getStart());
1179					}
1180				}
1181			}
1182			checkDeletedFiles(conversation);
1183			this.conversations.add(conversation);
1184			updateConversationUi();
1185			return conversation;
1186		}
1187	}
1188
1189	public void archiveConversation(Conversation conversation) {
1190		getNotificationService().clear(conversation);
1191		conversation.setStatus(Conversation.STATUS_ARCHIVED);
1192		conversation.setNextEncryption(-1);
1193		synchronized (this.conversations) {
1194			if (conversation.getMode() == Conversation.MODE_MULTI) {
1195				if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1196					Bookmark bookmark = conversation.getBookmark();
1197					if (bookmark != null && bookmark.autojoin()) {
1198						bookmark.setAutojoin(false);
1199						pushBookmarks(bookmark.getAccount());
1200					}
1201				}
1202				leaveMuc(conversation);
1203			} else {
1204				conversation.endOtrIfNeeded();
1205			}
1206			this.databaseBackend.updateConversation(conversation);
1207			this.conversations.remove(conversation);
1208			updateConversationUi();
1209		}
1210	}
1211
1212	public void createAccount(final Account account) {
1213		account.initAccountServices(this);
1214		databaseBackend.createAccount(account);
1215		this.accounts.add(account);
1216		this.reconnectAccountInBackground(account);
1217		updateAccountUi();
1218	}
1219
1220	public void updateAccount(final Account account) {
1221		this.statusListener.onStatusChanged(account);
1222		databaseBackend.updateAccount(account);
1223		reconnectAccount(account, false);
1224		updateAccountUi();
1225		getNotificationService().updateErrorNotification();
1226	}
1227
1228	public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
1229		final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
1230		sendIqPacket(account, iq, new OnIqPacketReceived() {
1231			@Override
1232			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1233				if (packet.getType() == IqPacket.TYPE.RESULT) {
1234					account.setPassword(newPassword);
1235					databaseBackend.updateAccount(account);
1236					callback.onPasswordChangeSucceeded();
1237				} else {
1238					callback.onPasswordChangeFailed();
1239				}
1240			}
1241		});
1242	}
1243
1244	public void deleteAccount(final Account account) {
1245		synchronized (this.conversations) {
1246			for (final Conversation conversation : conversations) {
1247				if (conversation.getAccount() == account) {
1248					if (conversation.getMode() == Conversation.MODE_MULTI) {
1249						leaveMuc(conversation);
1250					} else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1251						conversation.endOtrIfNeeded();
1252					}
1253					conversations.remove(conversation);
1254				}
1255			}
1256			if (account.getXmppConnection() != null) {
1257				this.disconnect(account, true);
1258			}
1259			databaseBackend.deleteAccount(account);
1260			this.accounts.remove(account);
1261			updateAccountUi();
1262			getNotificationService().updateErrorNotification();
1263		}
1264	}
1265
1266	public void setOnConversationListChangedListener(OnConversationUpdate listener) {
1267		synchronized (this) {
1268			if (checkListeners()) {
1269				switchToForeground();
1270			}
1271			this.mOnConversationUpdate = listener;
1272			this.mNotificationService.setIsInForeground(true);
1273			if (this.convChangedListenerCount < 2) {
1274				this.convChangedListenerCount++;
1275			}
1276		}
1277	}
1278
1279	public void removeOnConversationListChangedListener() {
1280		synchronized (this) {
1281			this.convChangedListenerCount--;
1282			if (this.convChangedListenerCount <= 0) {
1283				this.convChangedListenerCount = 0;
1284				this.mOnConversationUpdate = null;
1285				this.mNotificationService.setIsInForeground(false);
1286				if (checkListeners()) {
1287					switchToBackground();
1288				}
1289			}
1290		}
1291	}
1292
1293	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1294		synchronized (this) {
1295			if (checkListeners()) {
1296				switchToForeground();
1297			}
1298			this.mOnAccountUpdate = listener;
1299			if (this.accountChangedListenerCount < 2) {
1300				this.accountChangedListenerCount++;
1301			}
1302		}
1303	}
1304
1305	public void removeOnAccountListChangedListener() {
1306		synchronized (this) {
1307			this.accountChangedListenerCount--;
1308			if (this.accountChangedListenerCount <= 0) {
1309				this.mOnAccountUpdate = null;
1310				this.accountChangedListenerCount = 0;
1311				if (checkListeners()) {
1312					switchToBackground();
1313				}
1314			}
1315		}
1316	}
1317
1318	public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
1319		synchronized (this) {
1320			if (checkListeners()) {
1321				switchToForeground();
1322			}
1323			this.mOnRosterUpdate = listener;
1324			if (this.rosterChangedListenerCount < 2) {
1325				this.rosterChangedListenerCount++;
1326			}
1327		}
1328	}
1329
1330	public void removeOnRosterUpdateListener() {
1331		synchronized (this) {
1332			this.rosterChangedListenerCount--;
1333			if (this.rosterChangedListenerCount <= 0) {
1334				this.rosterChangedListenerCount = 0;
1335				this.mOnRosterUpdate = null;
1336				if (checkListeners()) {
1337					switchToBackground();
1338				}
1339			}
1340		}
1341	}
1342
1343	public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
1344		synchronized (this) {
1345			if (checkListeners()) {
1346				switchToForeground();
1347			}
1348			this.mOnUpdateBlocklist = listener;
1349			if (this.updateBlocklistListenerCount < 2) {
1350				this.updateBlocklistListenerCount++;
1351			}
1352		}
1353	}
1354
1355	public void removeOnUpdateBlocklistListener() {
1356		synchronized (this) {
1357			this.updateBlocklistListenerCount--;
1358			if (this.updateBlocklistListenerCount <= 0) {
1359				this.updateBlocklistListenerCount = 0;
1360				this.mOnUpdateBlocklist = null;
1361				if (checkListeners()) {
1362					switchToBackground();
1363				}
1364			}
1365		}
1366	}
1367
1368	public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
1369		synchronized (this) {
1370			if (checkListeners()) {
1371				switchToForeground();
1372			}
1373			this.mOnMucRosterUpdate = listener;
1374			if (this.mucRosterChangedListenerCount < 2) {
1375				this.mucRosterChangedListenerCount++;
1376			}
1377		}
1378	}
1379
1380	public void removeOnMucRosterUpdateListener() {
1381		synchronized (this) {
1382			this.mucRosterChangedListenerCount--;
1383			if (this.mucRosterChangedListenerCount <= 0) {
1384				this.mucRosterChangedListenerCount = 0;
1385				this.mOnMucRosterUpdate = null;
1386				if (checkListeners()) {
1387					switchToBackground();
1388				}
1389			}
1390		}
1391	}
1392
1393	private boolean checkListeners() {
1394		return (this.mOnAccountUpdate == null
1395				&& this.mOnConversationUpdate == null
1396				&& this.mOnRosterUpdate == null
1397				&& this.mOnUpdateBlocklist == null);
1398	}
1399
1400	private void switchToForeground() {
1401		for (Account account : getAccounts()) {
1402			if (account.getStatus() == Account.State.ONLINE) {
1403				XmppConnection connection = account.getXmppConnection();
1404				if (connection != null && connection.getFeatures().csi()) {
1405					connection.sendActive();
1406				}
1407			}
1408		}
1409		Log.d(Config.LOGTAG, "app switched into foreground");
1410	}
1411
1412	private void switchToBackground() {
1413		for (Account account : getAccounts()) {
1414			if (account.getStatus() == Account.State.ONLINE) {
1415				XmppConnection connection = account.getXmppConnection();
1416				if (connection != null && connection.getFeatures().csi()) {
1417					connection.sendInactive();
1418				}
1419			}
1420		}
1421		for(Conversation conversation : getConversations()) {
1422			conversation.setIncomingChatState(ChatState.ACTIVE);
1423		}
1424		this.mNotificationService.setIsInForeground(false);
1425		Log.d(Config.LOGTAG, "app switched into background");
1426	}
1427
1428	private void connectMultiModeConversations(Account account) {
1429		List<Conversation> conversations = getConversations();
1430		for (Conversation conversation : conversations) {
1431			if ((conversation.getMode() == Conversation.MODE_MULTI)
1432					&& (conversation.getAccount() == account)) {
1433				conversation.resetMucOptions();
1434				joinMuc(conversation);
1435			}
1436		}
1437	}
1438
1439	public void joinMuc(Conversation conversation) {
1440		Account account = conversation.getAccount();
1441		account.pendingConferenceJoins.remove(conversation);
1442		account.pendingConferenceLeaves.remove(conversation);
1443		if (account.getStatus() == Account.State.ONLINE) {
1444			final String nick = conversation.getMucOptions().getProposedNick();
1445			final Jid joinJid = conversation.getMucOptions().createJoinJid(nick);
1446			if (joinJid == null) {
1447				return; //safety net
1448			}
1449			Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
1450			PresencePacket packet = new PresencePacket();
1451			packet.setFrom(conversation.getAccount().getJid());
1452			packet.setTo(joinJid);
1453			Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
1454			if (conversation.getMucOptions().getPassword() != null) {
1455				x.addChild("password").setContent(conversation.getMucOptions().getPassword());
1456			}
1457			x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted()));
1458			String sig = account.getPgpSignature();
1459			if (sig != null) {
1460				packet.addChild("status").setContent("online");
1461				packet.addChild("x", "jabber:x:signed").setContent(sig);
1462			}
1463			sendPresencePacket(account, packet);
1464			fetchConferenceConfiguration(conversation);
1465			if (!joinJid.equals(conversation.getJid())) {
1466				conversation.setContactJid(joinJid);
1467				databaseBackend.updateConversation(conversation);
1468			}
1469			conversation.setHasMessagesLeftOnServer(false);
1470		} else {
1471			account.pendingConferenceJoins.add(conversation);
1472		}
1473	}
1474
1475	public void providePasswordForMuc(Conversation conversation, String password) {
1476		if (conversation.getMode() == Conversation.MODE_MULTI) {
1477			conversation.getMucOptions().setPassword(password);
1478			if (conversation.getBookmark() != null) {
1479				conversation.getBookmark().setAutojoin(true);
1480				pushBookmarks(conversation.getAccount());
1481			}
1482			databaseBackend.updateConversation(conversation);
1483			joinMuc(conversation);
1484		}
1485	}
1486
1487	public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
1488		final MucOptions options = conversation.getMucOptions();
1489		final Jid joinJid = options.createJoinJid(nick);
1490		if (options.online()) {
1491			Account account = conversation.getAccount();
1492			options.setOnRenameListener(new OnRenameListener() {
1493
1494				@Override
1495				public void onSuccess() {
1496					conversation.setContactJid(joinJid);
1497					databaseBackend.updateConversation(conversation);
1498					Bookmark bookmark = conversation.getBookmark();
1499					if (bookmark != null) {
1500						bookmark.setNick(nick);
1501						pushBookmarks(bookmark.getAccount());
1502					}
1503					callback.success(conversation);
1504				}
1505
1506				@Override
1507				public void onFailure() {
1508					callback.error(R.string.nick_in_use, conversation);
1509				}
1510			});
1511
1512			PresencePacket packet = new PresencePacket();
1513			packet.setTo(joinJid);
1514			packet.setFrom(conversation.getAccount().getJid());
1515
1516			String sig = account.getPgpSignature();
1517			if (sig != null) {
1518				packet.addChild("status").setContent("online");
1519				packet.addChild("x", "jabber:x:signed").setContent(sig);
1520			}
1521			sendPresencePacket(account, packet);
1522		} else {
1523			conversation.setContactJid(joinJid);
1524			databaseBackend.updateConversation(conversation);
1525			if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1526				Bookmark bookmark = conversation.getBookmark();
1527				if (bookmark != null) {
1528					bookmark.setNick(nick);
1529					pushBookmarks(bookmark.getAccount());
1530				}
1531				joinMuc(conversation);
1532			}
1533		}
1534	}
1535
1536	public void leaveMuc(Conversation conversation) {
1537		Account account = conversation.getAccount();
1538		account.pendingConferenceJoins.remove(conversation);
1539		account.pendingConferenceLeaves.remove(conversation);
1540		if (account.getStatus() == Account.State.ONLINE) {
1541			PresencePacket packet = new PresencePacket();
1542			packet.setTo(conversation.getJid());
1543			packet.setFrom(conversation.getAccount().getJid());
1544			packet.setAttribute("type", "unavailable");
1545			sendPresencePacket(conversation.getAccount(), packet);
1546			conversation.getMucOptions().setOffline();
1547			conversation.deregisterWithBookmark();
1548			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1549					+ ": leaving muc " + conversation.getJid());
1550		} else {
1551			account.pendingConferenceLeaves.add(conversation);
1552		}
1553	}
1554
1555	private String findConferenceServer(final Account account) {
1556		String server;
1557		if (account.getXmppConnection() != null) {
1558			server = account.getXmppConnection().getMucServer();
1559			if (server != null) {
1560				return server;
1561			}
1562		}
1563		for (Account other : getAccounts()) {
1564			if (other != account && other.getXmppConnection() != null) {
1565				server = other.getXmppConnection().getMucServer();
1566				if (server != null) {
1567					return server;
1568				}
1569			}
1570		}
1571		return null;
1572	}
1573
1574	public void createAdhocConference(final Account account, final Iterable<Jid> jids, final UiCallback<Conversation> callback) {
1575		Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString());
1576		if (account.getStatus() == Account.State.ONLINE) {
1577			try {
1578				String server = findConferenceServer(account);
1579				if (server == null) {
1580					if (callback != null) {
1581						callback.error(R.string.no_conference_server_found, null);
1582					}
1583					return;
1584				}
1585				String name = new BigInteger(75, getRNG()).toString(32);
1586				Jid jid = Jid.fromParts(name, server, null);
1587				final Conversation conversation = findOrCreateConversation(account, jid, true);
1588				joinMuc(conversation);
1589				Bundle options = new Bundle();
1590				options.putString("muc#roomconfig_persistentroom", "1");
1591				options.putString("muc#roomconfig_membersonly", "1");
1592				options.putString("muc#roomconfig_publicroom", "0");
1593				options.putString("muc#roomconfig_whois", "anyone");
1594				pushConferenceConfiguration(conversation, options, new OnConferenceOptionsPushed() {
1595					@Override
1596					public void onPushSucceeded() {
1597						for (Jid invite : jids) {
1598							invite(conversation, invite);
1599						}
1600						if (account.countPresences() > 1) {
1601							directInvite(conversation, account.getJid().toBareJid());
1602						}
1603						if (callback != null) {
1604							callback.success(conversation);
1605						}
1606					}
1607
1608					@Override
1609					public void onPushFailed() {
1610						if (callback != null) {
1611							callback.error(R.string.conference_creation_failed, conversation);
1612						}
1613					}
1614				});
1615
1616			} catch (InvalidJidException e) {
1617				if (callback != null) {
1618					callback.error(R.string.conference_creation_failed, null);
1619				}
1620			}
1621		} else {
1622			if (callback != null) {
1623				callback.error(R.string.not_connected_try_again, null);
1624			}
1625		}
1626	}
1627
1628	public void fetchConferenceConfiguration(final Conversation conversation) {
1629		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1630		request.setTo(conversation.getJid().toBareJid());
1631		request.query("http://jabber.org/protocol/disco#info");
1632		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
1633			@Override
1634			public void onIqPacketReceived(Account account, IqPacket packet) {
1635				if (packet.getType() != IqPacket.TYPE.ERROR) {
1636					ArrayList<String> features = new ArrayList<>();
1637					for (Element child : packet.query().getChildren()) {
1638						if (child != null && child.getName().equals("feature")) {
1639							String var = child.getAttribute("var");
1640							if (var != null) {
1641								features.add(var);
1642							}
1643						}
1644					}
1645					conversation.getMucOptions().updateFeatures(features);
1646					updateConversationUi();
1647				}
1648			}
1649		});
1650	}
1651
1652	public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) {
1653		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1654		request.setTo(conversation.getJid().toBareJid());
1655		request.query("http://jabber.org/protocol/muc#owner");
1656		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
1657			@Override
1658			public void onIqPacketReceived(Account account, IqPacket packet) {
1659				if (packet.getType() != IqPacket.TYPE.ERROR) {
1660					Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
1661					for (Field field : data.getFields()) {
1662						if (options.containsKey(field.getName())) {
1663							field.setValue(options.getString(field.getName()));
1664						}
1665					}
1666					data.submit();
1667					IqPacket set = new IqPacket(IqPacket.TYPE.SET);
1668					set.setTo(conversation.getJid().toBareJid());
1669					set.query("http://jabber.org/protocol/muc#owner").addChild(data);
1670					sendIqPacket(account, set, new OnIqPacketReceived() {
1671						@Override
1672						public void onIqPacketReceived(Account account, IqPacket packet) {
1673							if (packet.getType() == IqPacket.TYPE.RESULT) {
1674								if (callback != null) {
1675									callback.onPushSucceeded();
1676								}
1677							} else {
1678								if (callback != null) {
1679									callback.onPushFailed();
1680								}
1681							}
1682						}
1683					});
1684				} else {
1685					if (callback != null) {
1686						callback.onPushFailed();
1687					}
1688				}
1689			}
1690		});
1691	}
1692
1693	public void pushSubjectToConference(final Conversation conference, final String subject) {
1694		MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject);
1695		this.sendMessagePacket(conference.getAccount(), packet);
1696		final MucOptions mucOptions = conference.getMucOptions();
1697		final MucOptions.User self = mucOptions.getSelf();
1698		if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
1699			Bundle options = new Bundle();
1700			options.putString("muc#roomconfig_persistentroom", "1");
1701			this.pushConferenceConfiguration(conference, options, null);
1702		}
1703	}
1704
1705	public void changeAffiliationInConference(final Conversation conference, Jid user, MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
1706		final Jid jid = user.toBareJid();
1707		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
1708		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
1709			@Override
1710			public void onIqPacketReceived(Account account, IqPacket packet) {
1711				if (packet.getType() == IqPacket.TYPE.RESULT) {
1712					callback.onAffiliationChangedSuccessful(jid);
1713				} else {
1714					callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
1715				}
1716			}
1717		});
1718	}
1719
1720	public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
1721		List<Jid> jids = new ArrayList<>();
1722		for (MucOptions.User user : conference.getMucOptions().getUsers()) {
1723			if (user.getAffiliation() == before) {
1724				jids.add(user.getJid());
1725			}
1726		}
1727		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
1728		sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
1729	}
1730
1731	public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
1732		IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
1733		Log.d(Config.LOGTAG, request.toString());
1734		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
1735			@Override
1736			public void onIqPacketReceived(Account account, IqPacket packet) {
1737				Log.d(Config.LOGTAG, packet.toString());
1738				if (packet.getType() == IqPacket.TYPE.RESULT) {
1739					callback.onRoleChangedSuccessful(nick);
1740				} else {
1741					callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
1742				}
1743			}
1744		});
1745	}
1746
1747	public void disconnect(Account account, boolean force) {
1748		if ((account.getStatus() == Account.State.ONLINE)
1749				|| (account.getStatus() == Account.State.DISABLED)) {
1750			if (!force) {
1751				List<Conversation> conversations = getConversations();
1752				for (Conversation conversation : conversations) {
1753					if (conversation.getAccount() == account) {
1754						if (conversation.getMode() == Conversation.MODE_MULTI) {
1755							leaveMuc(conversation);
1756						} else {
1757							if (conversation.endOtrIfNeeded()) {
1758								Log.d(Config.LOGTAG, account.getJid().toBareJid()
1759										+ ": ended otr session with "
1760										+ conversation.getJid());
1761							}
1762						}
1763					}
1764				}
1765				sendOfflinePresence(account);
1766			}
1767			account.getXmppConnection().disconnect(force);
1768		}
1769	}
1770
1771	@Override
1772	public IBinder onBind(Intent intent) {
1773		return mBinder;
1774	}
1775
1776	public void updateMessage(Message message) {
1777		databaseBackend.updateMessage(message);
1778		updateConversationUi();
1779	}
1780
1781	protected void syncDirtyContacts(Account account) {
1782		for (Contact contact : account.getRoster().getContacts()) {
1783			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1784				pushContactToServer(contact);
1785			}
1786			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1787				deleteContactOnServer(contact);
1788			}
1789		}
1790	}
1791
1792	public void createContact(Contact contact) {
1793		SharedPreferences sharedPref = getPreferences();
1794		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1795		if (autoGrant) {
1796			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1797			contact.setOption(Contact.Options.ASKING);
1798		}
1799		pushContactToServer(contact);
1800	}
1801
1802	public void onOtrSessionEstablished(Conversation conversation) {
1803		final Account account = conversation.getAccount();
1804		final Session otrSession = conversation.getOtrSession();
1805		Log.d(Config.LOGTAG,
1806				account.getJid().toBareJid() + " otr session established with "
1807						+ conversation.getJid() + "/"
1808						+ otrSession.getSessionID().getUserID());
1809		conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
1810
1811			@Override
1812			public void onMessageFound(Message message) {
1813				SessionID id = otrSession.getSessionID();
1814				try {
1815					message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
1816				} catch (InvalidJidException e) {
1817					return;
1818				}
1819				if (message.needsUploading()) {
1820					mJingleConnectionManager.createNewConnection(message);
1821				} else {
1822					MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true);
1823					if (outPacket != null) {
1824						message.setStatus(Message.STATUS_SEND);
1825						databaseBackend.updateMessage(message);
1826						sendMessagePacket(account, outPacket);
1827					}
1828				}
1829				updateConversationUi();
1830			}
1831		});
1832	}
1833
1834	public boolean renewSymmetricKey(Conversation conversation) {
1835		Account account = conversation.getAccount();
1836		byte[] symmetricKey = new byte[32];
1837		this.mRandom.nextBytes(symmetricKey);
1838		Session otrSession = conversation.getOtrSession();
1839		if (otrSession != null) {
1840			MessagePacket packet = new MessagePacket();
1841			packet.setType(MessagePacket.TYPE_CHAT);
1842			packet.setFrom(account.getJid());
1843			packet.addChild("private", "urn:xmpp:carbons:2");
1844			packet.addChild("no-copy", "urn:xmpp:hints");
1845			packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
1846					+ otrSession.getSessionID().getUserID());
1847			try {
1848				packet.setBody(otrSession
1849						.transformSending(CryptoHelper.FILETRANSFER
1850								+ CryptoHelper.bytesToHex(symmetricKey))[0]);
1851				sendMessagePacket(account, packet);
1852				conversation.setSymmetricKey(symmetricKey);
1853				return true;
1854			} catch (OtrException e) {
1855				return false;
1856			}
1857		}
1858		return false;
1859	}
1860
1861	public void pushContactToServer(final Contact contact) {
1862		contact.resetOption(Contact.Options.DIRTY_DELETE);
1863		contact.setOption(Contact.Options.DIRTY_PUSH);
1864		final Account account = contact.getAccount();
1865		if (account.getStatus() == Account.State.ONLINE) {
1866			final boolean ask = contact.getOption(Contact.Options.ASKING);
1867			final boolean sendUpdates = contact
1868					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
1869					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
1870			final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1871			iq.query(Xmlns.ROSTER).addChild(contact.asElement());
1872			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
1873			if (sendUpdates) {
1874				sendPresencePacket(account,
1875						mPresenceGenerator.sendPresenceUpdatesTo(contact));
1876			}
1877			if (ask) {
1878				sendPresencePacket(account,
1879						mPresenceGenerator.requestPresenceUpdatesFrom(contact));
1880			}
1881		}
1882	}
1883
1884	public void publishAvatar(final Account account,
1885							  final Uri image,
1886							  final UiCallback<Avatar> callback) {
1887		final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
1888		final int size = Config.AVATAR_SIZE;
1889		final Avatar avatar = getFileBackend()
1890				.getPepAvatar(image, size, format);
1891		if (avatar != null) {
1892			avatar.height = size;
1893			avatar.width = size;
1894			if (format.equals(Bitmap.CompressFormat.WEBP)) {
1895				avatar.type = "image/webp";
1896			} else if (format.equals(Bitmap.CompressFormat.JPEG)) {
1897				avatar.type = "image/jpeg";
1898			} else if (format.equals(Bitmap.CompressFormat.PNG)) {
1899				avatar.type = "image/png";
1900			}
1901			if (!getFileBackend().save(avatar)) {
1902				callback.error(R.string.error_saving_avatar, avatar);
1903				return;
1904			}
1905			final IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
1906			this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1907
1908				@Override
1909				public void onIqPacketReceived(Account account, IqPacket result) {
1910					if (result.getType() == IqPacket.TYPE.RESULT) {
1911						final IqPacket packet = XmppConnectionService.this.mIqGenerator
1912								.publishAvatarMetadata(avatar);
1913						sendIqPacket(account, packet, new OnIqPacketReceived() {
1914
1915							@Override
1916							public void onIqPacketReceived(Account account,
1917														   IqPacket result) {
1918								if (result.getType() == IqPacket.TYPE.RESULT) {
1919									if (account.setAvatar(avatar.getFilename())) {
1920										getAvatarService().clear(account);
1921										databaseBackend.updateAccount(account);
1922									}
1923									callback.success(avatar);
1924								} else {
1925									callback.error(
1926											R.string.error_publish_avatar_server_reject,
1927											avatar);
1928								}
1929							}
1930						});
1931					} else {
1932						callback.error(
1933								R.string.error_publish_avatar_server_reject,
1934								avatar);
1935					}
1936				}
1937			});
1938		} else {
1939			callback.error(R.string.error_publish_avatar_converting, null);
1940		}
1941	}
1942
1943	public void fetchAvatar(Account account, Avatar avatar) {
1944		fetchAvatar(account, avatar, null);
1945	}
1946
1947	private static String generateFetchKey(Account account, final Avatar avatar) {
1948		return account.getJid().toBareJid()+"_"+avatar.owner+"_"+avatar.sha1sum;
1949	}
1950
1951	public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
1952		final String KEY = generateFetchKey(account, avatar);
1953		synchronized(this.mInProgressAvatarFetches) {
1954			if (this.mInProgressAvatarFetches.contains(KEY)) {
1955				return;
1956			} else {
1957				switch (avatar.origin) {
1958					case PEP:
1959						this.mInProgressAvatarFetches.add(KEY);
1960						fetchAvatarPep(account, avatar, callback);
1961						break;
1962					case VCARD:
1963						this.mInProgressAvatarFetches.add(KEY);
1964						fetchAvatarVcard(account, avatar, callback);
1965						break;
1966				}
1967			}
1968		}
1969	}
1970
1971	private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
1972		IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
1973		sendIqPacket(account, packet, new OnIqPacketReceived() {
1974
1975			@Override
1976			public void onIqPacketReceived(Account account, IqPacket result) {
1977				synchronized (mInProgressAvatarFetches) {
1978					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
1979				}
1980				final String ERROR = account.getJid().toBareJid()
1981						+ ": fetching avatar for " + avatar.owner + " failed ";
1982				if (result.getType() == IqPacket.TYPE.RESULT) {
1983					avatar.image = mIqParser.avatarData(result);
1984					if (avatar.image != null) {
1985						if (getFileBackend().save(avatar)) {
1986							if (account.getJid().toBareJid().equals(avatar.owner)) {
1987								if (account.setAvatar(avatar.getFilename())) {
1988									databaseBackend.updateAccount(account);
1989								}
1990								getAvatarService().clear(account);
1991								updateConversationUi();
1992								updateAccountUi();
1993							} else {
1994								Contact contact = account.getRoster()
1995										.getContact(avatar.owner);
1996								contact.setAvatar(avatar);
1997								getAvatarService().clear(contact);
1998								updateConversationUi();
1999								updateRosterUi();
2000							}
2001							if (callback != null) {
2002								callback.success(avatar);
2003							}
2004							Log.d(Config.LOGTAG, account.getJid().toBareJid()
2005									+ ": succesfuly fetched pep avatar for " + avatar.owner);
2006							return;
2007						}
2008					} else {
2009
2010						Log.d(Config.LOGTAG, ERROR + "(parsing error)");
2011					}
2012				} else {
2013					Element error = result.findChild("error");
2014					if (error == null) {
2015						Log.d(Config.LOGTAG, ERROR + "(server error)");
2016					} else {
2017						Log.d(Config.LOGTAG, ERROR + error.toString());
2018					}
2019				}
2020				if (callback != null) {
2021					callback.error(0, null);
2022				}
2023
2024			}
2025		});
2026	}
2027
2028	private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2029		IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
2030		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2031			@Override
2032			public void onIqPacketReceived(Account account, IqPacket packet) {
2033				synchronized (mInProgressAvatarFetches) {
2034					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2035				}
2036				if (packet.getType() == IqPacket.TYPE.RESULT) {
2037					Element vCard = packet.findChild("vCard", "vcard-temp");
2038					Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
2039					String image = photo != null ? photo.findChildContent("BINVAL") : null;
2040					if (image != null) {
2041						avatar.image = image;
2042						if (getFileBackend().save(avatar)) {
2043							Log.d(Config.LOGTAG, account.getJid().toBareJid()
2044									+ ": successfully fetched vCard avatar for " + avatar.owner);
2045							Contact contact = account.getRoster()
2046									.getContact(avatar.owner);
2047							contact.setAvatar(avatar);
2048							getAvatarService().clear(contact);
2049							updateConversationUi();
2050							updateRosterUi();
2051						}
2052					}
2053				}
2054			}
2055		});
2056	}
2057
2058	public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
2059		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
2060		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2061
2062			@Override
2063			public void onIqPacketReceived(Account account, IqPacket packet) {
2064				if (packet.getType() == IqPacket.TYPE.RESULT) {
2065					Element pubsub = packet.findChild("pubsub",
2066							"http://jabber.org/protocol/pubsub");
2067					if (pubsub != null) {
2068						Element items = pubsub.findChild("items");
2069						if (items != null) {
2070							Avatar avatar = Avatar.parseMetadata(items);
2071							if (avatar != null) {
2072								avatar.owner = account.getJid().toBareJid();
2073								if (fileBackend.isAvatarCached(avatar)) {
2074									if (account.setAvatar(avatar.getFilename())) {
2075										databaseBackend.updateAccount(account);
2076									}
2077									getAvatarService().clear(account);
2078									callback.success(avatar);
2079								} else {
2080									fetchAvatarPep(account, avatar, callback);
2081								}
2082								return;
2083							}
2084						}
2085					}
2086				}
2087				callback.error(0, null);
2088			}
2089		});
2090	}
2091
2092	public void deleteContactOnServer(Contact contact) {
2093		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
2094		contact.resetOption(Contact.Options.DIRTY_PUSH);
2095		contact.setOption(Contact.Options.DIRTY_DELETE);
2096		Account account = contact.getAccount();
2097		if (account.getStatus() == Account.State.ONLINE) {
2098			IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2099			Element item = iq.query(Xmlns.ROSTER).addChild("item");
2100			item.setAttribute("jid", contact.getJid().toString());
2101			item.setAttribute("subscription", "remove");
2102			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2103		}
2104	}
2105
2106	public void updateConversation(Conversation conversation) {
2107		this.databaseBackend.updateConversation(conversation);
2108	}
2109
2110	public void reconnectAccount(final Account account, final boolean force) {
2111		synchronized (account) {
2112			if (account.getXmppConnection() != null) {
2113				disconnect(account, force);
2114			}
2115			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2116
2117				synchronized (this.mInProgressAvatarFetches) {
2118					for(Iterator<String> iterator = this.mInProgressAvatarFetches.iterator(); iterator.hasNext();) {
2119						final String KEY = iterator.next();
2120						if (KEY.startsWith(account.getJid().toBareJid()+"_")) {
2121							iterator.remove();
2122						}
2123					}
2124				}
2125
2126				if (account.getXmppConnection() == null) {
2127					account.setXmppConnection(createConnection(account));
2128				}
2129				Thread thread = new Thread(account.getXmppConnection());
2130				thread.start();
2131				scheduleWakeUpCall(Config.CONNECT_TIMEOUT, account.getUuid().hashCode());
2132			} else {
2133				account.getRoster().clearPresences();
2134				account.setXmppConnection(null);
2135			}
2136		}
2137	}
2138
2139	public void reconnectAccountInBackground(final Account account) {
2140		new Thread(new Runnable() {
2141			@Override
2142			public void run() {
2143				reconnectAccount(account,false);
2144			}
2145		}).start();
2146	}
2147
2148	public void invite(Conversation conversation, Jid contact) {
2149		Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+": inviting "+contact+" to "+conversation.getJid().toBareJid());
2150		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
2151		sendMessagePacket(conversation.getAccount(), packet);
2152	}
2153
2154	public void directInvite(Conversation conversation, Jid jid) {
2155		MessagePacket packet = mMessageGenerator.directInvite(conversation, jid);
2156		sendMessagePacket(conversation.getAccount(),packet);
2157	}
2158
2159	public void resetSendingToWaiting(Account account) {
2160		for (Conversation conversation : getConversations()) {
2161			if (conversation.getAccount() == account) {
2162				conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
2163
2164					@Override
2165					public void onMessageFound(Message message) {
2166						markMessage(message, Message.STATUS_WAITING);
2167					}
2168				});
2169			}
2170		}
2171	}
2172
2173	public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
2174		if (uuid == null) {
2175			return null;
2176		}
2177		for (Conversation conversation : getConversations()) {
2178			if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) {
2179				final Message message = conversation.findSentMessageWithUuid(uuid);
2180				if (message != null) {
2181					markMessage(message, status);
2182				}
2183				return message;
2184			}
2185		}
2186		return null;
2187	}
2188
2189	public boolean markMessage(Conversation conversation, String uuid,
2190							   int status) {
2191		if (uuid == null) {
2192			return false;
2193		} else {
2194			Message message = conversation.findSentMessageWithUuid(uuid);
2195			if (message != null) {
2196				markMessage(message, status);
2197				return true;
2198			} else {
2199				return false;
2200			}
2201		}
2202	}
2203
2204	public void markMessage(Message message, int status) {
2205		if (status == Message.STATUS_SEND_FAILED
2206				&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
2207				.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
2208			return;
2209		}
2210		message.setStatus(status);
2211		databaseBackend.updateMessage(message);
2212		updateConversationUi();
2213	}
2214
2215	public SharedPreferences getPreferences() {
2216		return PreferenceManager
2217				.getDefaultSharedPreferences(getApplicationContext());
2218	}
2219
2220	public boolean forceEncryption() {
2221		return getPreferences().getBoolean("force_encryption", false);
2222	}
2223
2224	public boolean confirmMessages() {
2225		return getPreferences().getBoolean("confirm_messages", true);
2226	}
2227
2228	public boolean sendChatStates() {
2229		return getPreferences().getBoolean("chat_states", false);
2230	}
2231
2232	public boolean saveEncryptedMessages() {
2233		return !getPreferences().getBoolean("dont_save_encrypted", false);
2234	}
2235
2236	public boolean indicateReceived() {
2237		return getPreferences().getBoolean("indicate_received", false);
2238	}
2239
2240	public int unreadCount() {
2241		int count = 0;
2242		for(Conversation conversation : getConversations()) {
2243			count += conversation.unreadCount();
2244		}
2245		return count;
2246	}
2247
2248	public void updateConversationUi() {
2249		if (mOnConversationUpdate != null) {
2250			mOnConversationUpdate.onConversationUpdate();
2251		}
2252	}
2253
2254	public void updateAccountUi() {
2255		if (mOnAccountUpdate != null) {
2256			mOnAccountUpdate.onAccountUpdate();
2257		}
2258	}
2259
2260	public void updateRosterUi() {
2261		if (mOnRosterUpdate != null) {
2262			mOnRosterUpdate.onRosterUpdate();
2263		}
2264	}
2265
2266	public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
2267		if (mOnUpdateBlocklist != null) {
2268			mOnUpdateBlocklist.OnUpdateBlocklist(status);
2269		}
2270	}
2271
2272	public void updateMucRosterUi() {
2273		if (mOnMucRosterUpdate != null) {
2274			mOnMucRosterUpdate.onMucRosterUpdate();
2275		}
2276	}
2277
2278	public Account findAccountByJid(final Jid accountJid) {
2279		for (Account account : this.accounts) {
2280			if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
2281				return account;
2282			}
2283		}
2284		return null;
2285	}
2286
2287	public Conversation findConversationByUuid(String uuid) {
2288		for (Conversation conversation : getConversations()) {
2289			if (conversation.getUuid().equals(uuid)) {
2290				return conversation;
2291			}
2292		}
2293		return null;
2294	}
2295
2296	public void markRead(final Conversation conversation) {
2297		mNotificationService.clear(conversation);
2298		conversation.markRead();
2299		updateUnreadCountBadge();
2300	}
2301
2302	public synchronized void updateUnreadCountBadge() {
2303		int count = unreadCount();
2304		if (unreadCount != count) {
2305			Log.d(Config.LOGTAG, "update unread count to " + count);
2306			if (count > 0) {
2307				ShortcutBadger.with(getApplicationContext()).count(count);
2308			} else {
2309				ShortcutBadger.with(getApplicationContext()).remove();
2310			}
2311			unreadCount = count;
2312		}
2313	}
2314
2315	public void sendReadMarker(final Conversation conversation) {
2316		final Message markable = conversation.getLatestMarkableMessage();
2317		this.markRead(conversation);
2318		if (confirmMessages() && markable != null && markable.getRemoteMsgId() != null) {
2319			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
2320			Account account = conversation.getAccount();
2321			final Jid to = markable.getCounterpart();
2322			MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
2323			this.sendMessagePacket(conversation.getAccount(), packet);
2324		}
2325		updateConversationUi();
2326	}
2327
2328	public SecureRandom getRNG() {
2329		return this.mRandom;
2330	}
2331
2332	public MemorizingTrustManager getMemorizingTrustManager() {
2333		return this.mMemorizingTrustManager;
2334	}
2335
2336	public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
2337		this.mMemorizingTrustManager = trustManager;
2338	}
2339
2340	public void updateMemorizingTrustmanager() {
2341		final MemorizingTrustManager tm;
2342		final boolean dontTrustSystemCAs = getPreferences().getBoolean("dont_trust_system_cas", false);
2343		if (dontTrustSystemCAs) {
2344			 tm = new MemorizingTrustManager(getApplicationContext(), null);
2345		} else {
2346			tm = new MemorizingTrustManager(getApplicationContext());
2347		}
2348		setMemorizingTrustManager(tm);
2349	}
2350
2351	public PowerManager getPowerManager() {
2352		return this.pm;
2353	}
2354
2355	public LruCache<String, Bitmap> getBitmapCache() {
2356		return this.mBitmapCache;
2357	}
2358
2359	public void syncRosterToDisk(final Account account) {
2360		Runnable runnable = new Runnable() {
2361
2362			@Override
2363			public void run() {
2364				databaseBackend.writeRoster(account.getRoster());
2365			}
2366		};
2367		mDatabaseExecutor.execute(runnable);
2368
2369	}
2370
2371	public List<String> getKnownHosts() {
2372		final List<String> hosts = new ArrayList<>();
2373		for (final Account account : getAccounts()) {
2374			if (!hosts.contains(account.getServer().toString())) {
2375				hosts.add(account.getServer().toString());
2376			}
2377			for (final Contact contact : account.getRoster().getContacts()) {
2378				if (contact.showInRoster()) {
2379					final String server = contact.getServer().toString();
2380					if (server != null && !hosts.contains(server)) {
2381						hosts.add(server);
2382					}
2383				}
2384			}
2385		}
2386		return hosts;
2387	}
2388
2389	public List<String> getKnownConferenceHosts() {
2390		final ArrayList<String> mucServers = new ArrayList<>();
2391		for (final Account account : accounts) {
2392			if (account.getXmppConnection() != null) {
2393				final String server = account.getXmppConnection().getMucServer();
2394				if (server != null && !mucServers.contains(server)) {
2395					mucServers.add(server);
2396				}
2397			}
2398		}
2399		return mucServers;
2400	}
2401
2402	public void sendMessagePacket(Account account, MessagePacket packet) {
2403		XmppConnection connection = account.getXmppConnection();
2404		if (connection != null) {
2405			connection.sendMessagePacket(packet);
2406		}
2407	}
2408
2409	public void sendPresencePacket(Account account, PresencePacket packet) {
2410		XmppConnection connection = account.getXmppConnection();
2411		if (connection != null) {
2412			connection.sendPresencePacket(packet);
2413		}
2414	}
2415
2416	public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
2417		final XmppConnection connection = account.getXmppConnection();
2418		if (connection != null) {
2419			connection.sendIqPacket(packet, callback);
2420		}
2421	}
2422
2423	public void sendPresence(final Account account) {
2424		sendPresencePacket(account, mPresenceGenerator.sendPresence(account));
2425	}
2426
2427	public void sendOfflinePresence(final Account account) {
2428		sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
2429	}
2430
2431	public MessageGenerator getMessageGenerator() {
2432		return this.mMessageGenerator;
2433	}
2434
2435	public PresenceGenerator getPresenceGenerator() {
2436		return this.mPresenceGenerator;
2437	}
2438
2439	public IqGenerator getIqGenerator() {
2440		return this.mIqGenerator;
2441	}
2442
2443	public IqParser getIqParser() {
2444		return this.mIqParser;
2445	}
2446
2447	public JingleConnectionManager getJingleConnectionManager() {
2448		return this.mJingleConnectionManager;
2449	}
2450
2451	public MessageArchiveService getMessageArchiveService() {
2452		return this.mMessageArchiveService;
2453	}
2454
2455	public List<Contact> findContacts(Jid jid) {
2456		ArrayList<Contact> contacts = new ArrayList<>();
2457		for (Account account : getAccounts()) {
2458			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2459				Contact contact = account.getRoster().getContactFromRoster(jid);
2460				if (contact != null) {
2461					contacts.add(contact);
2462				}
2463			}
2464		}
2465		return contacts;
2466	}
2467
2468	public NotificationService getNotificationService() {
2469		return this.mNotificationService;
2470	}
2471
2472	public HttpConnectionManager getHttpConnectionManager() {
2473		return this.mHttpConnectionManager;
2474	}
2475
2476	public void resendFailedMessages(final Message message) {
2477		final Collection<Message> messages = new ArrayList<>();
2478		Message current = message;
2479		while (current.getStatus() == Message.STATUS_SEND_FAILED) {
2480			messages.add(current);
2481			if (current.mergeable(current.next())) {
2482				current = current.next();
2483			} else {
2484				break;
2485			}
2486		}
2487		for (final Message msg : messages) {
2488			markMessage(msg, Message.STATUS_WAITING);
2489			this.resendMessage(msg);
2490		}
2491	}
2492
2493	public void clearConversationHistory(final Conversation conversation) {
2494		conversation.clearMessages();
2495		conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
2496		new Thread(new Runnable() {
2497			@Override
2498			public void run() {
2499				databaseBackend.deleteMessagesInConversation(conversation);
2500			}
2501		}).start();
2502	}
2503
2504	public void sendBlockRequest(final Blockable blockable) {
2505		if (blockable != null && blockable.getBlockedJid() != null) {
2506			final Jid jid = blockable.getBlockedJid();
2507			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() {
2508
2509				@Override
2510				public void onIqPacketReceived(final Account account, final IqPacket packet) {
2511					if (packet.getType() == IqPacket.TYPE.RESULT) {
2512						account.getBlocklist().add(jid);
2513						updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
2514					}
2515				}
2516			});
2517		}
2518	}
2519
2520	public void sendUnblockRequest(final Blockable blockable) {
2521		if (blockable != null && blockable.getJid() != null) {
2522			final Jid jid = blockable.getBlockedJid();
2523			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
2524				@Override
2525				public void onIqPacketReceived(final Account account, final IqPacket packet) {
2526					if (packet.getType() == IqPacket.TYPE.RESULT) {
2527						account.getBlocklist().remove(jid);
2528						updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
2529					}
2530				}
2531			});
2532		}
2533	}
2534
2535	public interface OnMoreMessagesLoaded {
2536		public void onMoreMessagesLoaded(int count, Conversation conversation);
2537
2538		public void informUser(int r);
2539	}
2540
2541	public interface OnAccountPasswordChanged {
2542		public void onPasswordChangeSucceeded();
2543
2544		public void onPasswordChangeFailed();
2545	}
2546
2547	public interface OnAffiliationChanged {
2548		public void onAffiliationChangedSuccessful(Jid jid);
2549
2550		public void onAffiliationChangeFailed(Jid jid, int resId);
2551	}
2552
2553	public interface OnRoleChanged {
2554		public void onRoleChangedSuccessful(String nick);
2555
2556		public void onRoleChangeFailed(String nick, int resid);
2557	}
2558
2559	public interface OnConversationUpdate {
2560		public void onConversationUpdate();
2561	}
2562
2563	public interface OnAccountUpdate {
2564		public void onAccountUpdate();
2565	}
2566
2567	public interface OnRosterUpdate {
2568		public void onRosterUpdate();
2569	}
2570
2571	public interface OnMucRosterUpdate {
2572		public void onMucRosterUpdate();
2573	}
2574
2575	public interface OnConferenceOptionsPushed {
2576		public void onPushSucceeded();
2577
2578		public void onPushFailed();
2579	}
2580
2581	public class XmppConnectionBinder extends Binder {
2582		public XmppConnectionService getService() {
2583			return XmppConnectionService.this;
2584		}
2585	}
2586}