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