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					if (message.needsUploading()) {
 759						if (account.httpUploadAvailable() || message.fixCounterpart()) {
 760							this.sendFileMessage(message);
 761						} else {
 762							break;
 763						}
 764					} else {
 765						packet = account.getAxolotlService().fetchPacketFromCache(message);
 766						if (packet == null) {
 767							account.getAxolotlService().prepareMessage(message);
 768							message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
 769						}
 770					}
 771					break;
 772
 773			}
 774			if (packet != null) {
 775				if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
 776					message.setStatus(Message.STATUS_UNSEND);
 777				} else {
 778					message.setStatus(Message.STATUS_SEND);
 779				}
 780			}
 781		} else {
 782			switch(message.getEncryption()) {
 783				case Message.ENCRYPTION_DECRYPTED:
 784					if (!message.needsUploading()) {
 785						String pgpBody = message.getEncryptedBody();
 786						String decryptedBody = message.getBody();
 787						message.setBody(pgpBody);
 788						message.setEncryption(Message.ENCRYPTION_PGP);
 789						databaseBackend.createMessage(message);
 790						saveInDb = false;
 791						message.setBody(decryptedBody);
 792						message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 793					}
 794					break;
 795				case Message.ENCRYPTION_OTR:
 796					if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) {
 797						conversation.startOtrSession(message.getCounterpart().getResourcepart(), false);
 798					}
 799					break;
 800				case Message.ENCRYPTION_AXOLOTL:
 801					message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
 802					break;
 803			}
 804		}
 805
 806		if (resend) {
 807			if (packet != null) {
 808				if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
 809					markMessage(message,Message.STATUS_UNSEND);
 810				} else {
 811					markMessage(message,Message.STATUS_SEND);
 812				}
 813			}
 814		} else {
 815			conversation.add(message);
 816			if (saveInDb && (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages())) {
 817				databaseBackend.createMessage(message);
 818			}
 819			updateConversationUi();
 820		}
 821		if (packet != null) {
 822			if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
 823				if (this.sendChatStates()) {
 824					packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
 825				}
 826			}
 827			sendMessagePacket(account, packet);
 828		}
 829	}
 830
 831	private void sendUnsentMessages(final Conversation conversation) {
 832		conversation.findWaitingMessages(new Conversation.OnMessageFound() {
 833
 834			@Override
 835			public void onMessageFound(Message message) {
 836				resendMessage(message);
 837			}
 838		});
 839	}
 840
 841	public void resendMessage(final Message message) {
 842		sendMessage(message, true);
 843	}
 844
 845	public void fetchRosterFromServer(final Account account) {
 846		final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
 847		if (!"".equals(account.getRosterVersion())) {
 848			Log.d(Config.LOGTAG, account.getJid().toBareJid()
 849					+ ": fetching roster version " + account.getRosterVersion());
 850		} else {
 851			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
 852		}
 853		iqPacket.query(Xmlns.ROSTER).setAttribute("ver",account.getRosterVersion());
 854		sendIqPacket(account,iqPacket,mIqParser);
 855	}
 856
 857	public void fetchBookmarks(final Account account) {
 858		final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
 859		final Element query = iqPacket.query("jabber:iq:private");
 860		query.addChild("storage", "storage:bookmarks");
 861		final OnIqPacketReceived callback = new OnIqPacketReceived() {
 862
 863			@Override
 864			public void onIqPacketReceived(final Account account, final IqPacket packet) {
 865				final Element query = packet.query();
 866				final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
 867				final Element storage = query.findChild("storage",
 868						"storage:bookmarks");
 869				if (storage != null) {
 870					for (final Element item : storage.getChildren()) {
 871						if (item.getName().equals("conference")) {
 872							final Bookmark bookmark = Bookmark.parse(item, account);
 873							bookmarks.add(bookmark);
 874							Conversation conversation = find(bookmark);
 875							if (conversation != null) {
 876								conversation.setBookmark(bookmark);
 877							} else if (bookmark.autojoin() && bookmark.getJid() != null) {
 878								conversation = findOrCreateConversation(
 879										account, bookmark.getJid(), true);
 880								conversation.setBookmark(bookmark);
 881								joinMuc(conversation);
 882							}
 883						}
 884					}
 885				}
 886				account.setBookmarks(bookmarks);
 887			}
 888		};
 889		sendIqPacket(account, iqPacket, callback);
 890	}
 891
 892	public void pushBookmarks(Account account) {
 893		IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
 894		Element query = iqPacket.query("jabber:iq:private");
 895		Element storage = query.addChild("storage", "storage:bookmarks");
 896		for (Bookmark bookmark : account.getBookmarks()) {
 897			storage.addChild(bookmark);
 898		}
 899		sendIqPacket(account, iqPacket, mDefaultIqHandler);
 900	}
 901
 902	public void onPhoneContactsLoaded(final List<Bundle> phoneContacts) {
 903		if (mPhoneContactMergerThread != null) {
 904			mPhoneContactMergerThread.interrupt();
 905		}
 906		mPhoneContactMergerThread = new Thread(new Runnable() {
 907			@Override
 908			public void run() {
 909				Log.d(Config.LOGTAG,"start merging phone contacts with roster");
 910				for (Account account : accounts) {
 911					List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts();
 912					for (Bundle phoneContact : phoneContacts) {
 913						if (Thread.interrupted()) {
 914							Log.d(Config.LOGTAG,"interrupted merging phone contacts");
 915							return;
 916						}
 917						Jid jid;
 918						try {
 919							jid = Jid.fromString(phoneContact.getString("jid"));
 920						} catch (final InvalidJidException e) {
 921							continue;
 922						}
 923						final Contact contact = account.getRoster().getContact(jid);
 924						String systemAccount = phoneContact.getInt("phoneid")
 925							+ "#"
 926							+ phoneContact.getString("lookup");
 927						contact.setSystemAccount(systemAccount);
 928						if (contact.setPhotoUri(phoneContact.getString("photouri"))) {
 929							getAvatarService().clear(contact);
 930						}
 931						contact.setSystemName(phoneContact.getString("displayname"));
 932						withSystemAccounts.remove(contact);
 933					}
 934					for(Contact contact : withSystemAccounts) {
 935						contact.setSystemAccount(null);
 936						contact.setSystemName(null);
 937						if (contact.setPhotoUri(null)) {
 938							getAvatarService().clear(contact);
 939						}
 940					}
 941				}
 942				Log.d(Config.LOGTAG,"finished merging phone contacts");
 943				updateAccountUi();
 944			}
 945		});
 946		mPhoneContactMergerThread.start();
 947	}
 948
 949	private void restoreFromDatabase() {
 950		synchronized (this.conversations) {
 951			final Map<String, Account> accountLookupTable = new Hashtable<>();
 952			for (Account account : this.accounts) {
 953				accountLookupTable.put(account.getUuid(), account);
 954			}
 955			this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
 956			for (Conversation conversation : this.conversations) {
 957				Account account = accountLookupTable.get(conversation.getAccountUuid());
 958				conversation.setAccount(account);
 959			}
 960			Runnable runnable =new Runnable() {
 961				@Override
 962				public void run() {
 963					Log.d(Config.LOGTAG,"restoring roster");
 964					for(Account account : accounts) {
 965						databaseBackend.readRoster(account.getRoster());
 966						account.initAccountServices(XmppConnectionService.this);
 967					}
 968					getBitmapCache().evictAll();
 969					Looper.prepare();
 970					PhoneHelper.loadPhoneContacts(getApplicationContext(),
 971							new CopyOnWriteArrayList<Bundle>(),
 972							XmppConnectionService.this);
 973					Log.d(Config.LOGTAG,"restoring messages");
 974					for (Conversation conversation : conversations) {
 975						conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
 976						checkDeletedFiles(conversation);
 977					}
 978					mRestoredFromDatabase = true;
 979					Log.d(Config.LOGTAG,"restored all messages");
 980					updateConversationUi();
 981				}
 982			};
 983			mDatabaseExecutor.execute(runnable);
 984		}
 985	}
 986
 987	public List<Conversation> getConversations() {
 988		return this.conversations;
 989	}
 990
 991	private void checkDeletedFiles(Conversation conversation) {
 992		conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
 993
 994			@Override
 995			public void onMessageFound(Message message) {
 996				if (!getFileBackend().isFileAvailable(message)) {
 997					message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
 998				}
 999			}
1000		});
1001	}
1002
1003	private void markFileDeleted(String uuid) {
1004		for (Conversation conversation : getConversations()) {
1005			Message message = conversation.findMessageWithFileAndUuid(uuid);
1006			if (message != null) {
1007				if (!getFileBackend().isFileAvailable(message)) {
1008					message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1009					updateConversationUi();
1010				}
1011				return;
1012			}
1013		}
1014	}
1015
1016	public void populateWithOrderedConversations(final List<Conversation> list) {
1017		populateWithOrderedConversations(list, true);
1018	}
1019
1020	public void populateWithOrderedConversations(final List<Conversation> list, boolean includeNoFileUpload) {
1021		list.clear();
1022		if (includeNoFileUpload) {
1023			list.addAll(getConversations());
1024		} else {
1025			for (Conversation conversation : getConversations()) {
1026				if (conversation.getMode() == Conversation.MODE_SINGLE
1027						|| conversation.getAccount().httpUploadAvailable()) {
1028					list.add(conversation);
1029				}
1030			}
1031		}
1032		Collections.sort(list, new Comparator<Conversation>() {
1033			@Override
1034			public int compare(Conversation lhs, Conversation rhs) {
1035				Message left = lhs.getLatestMessage();
1036				Message right = rhs.getLatestMessage();
1037				if (left.getTimeSent() > right.getTimeSent()) {
1038					return -1;
1039				} else if (left.getTimeSent() < right.getTimeSent()) {
1040					return 1;
1041				} else {
1042					return 0;
1043				}
1044			}
1045		});
1046	}
1047
1048	public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
1049		Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp));
1050		if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) {
1051			return;
1052		}
1053		Runnable runnable = new Runnable() {
1054			@Override
1055			public void run() {
1056				final Account account = conversation.getAccount();
1057				List<Message> messages = databaseBackend.getMessages(conversation, 50,timestamp);
1058				if (messages.size() > 0) {
1059					conversation.addAll(0, messages);
1060					checkDeletedFiles(conversation);
1061					callback.onMoreMessagesLoaded(messages.size(), conversation);
1062				} else if (conversation.hasMessagesLeftOnServer()
1063						&& account.isOnlineAndConnected()
1064						&& account.getXmppConnection().getFeatures().mam()) {
1065					MessageArchiveService.Query query = getMessageArchiveService().query(conversation,0,timestamp - 1);
1066					if (query != null) {
1067						query.setCallback(callback);
1068					}
1069					callback.informUser(R.string.fetching_history_from_server);
1070				}
1071			}
1072		};
1073		mDatabaseExecutor.execute(runnable);
1074	}
1075
1076	public List<Account> getAccounts() {
1077		return this.accounts;
1078	}
1079
1080	public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
1081		for (final Conversation conversation : haystack) {
1082			if (conversation.getContact() == contact) {
1083				return conversation;
1084			}
1085		}
1086		return null;
1087	}
1088
1089	public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
1090		if (jid == null) {
1091			return null;
1092		}
1093		for (final Conversation conversation : haystack) {
1094			if ((account == null || conversation.getAccount() == account)
1095					&& (conversation.getJid().toBareJid().equals(jid.toBareJid()))) {
1096				return conversation;
1097			}
1098		}
1099		return null;
1100	}
1101
1102	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) {
1103		return this.findOrCreateConversation(account, jid, muc, null);
1104	}
1105
1106	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) {
1107		synchronized (this.conversations) {
1108			Conversation conversation = find(account, jid);
1109			if (conversation != null) {
1110				return conversation;
1111			}
1112			conversation = databaseBackend.findConversation(account, jid);
1113			if (conversation != null) {
1114				conversation.setStatus(Conversation.STATUS_AVAILABLE);
1115				conversation.setAccount(account);
1116				if (muc) {
1117					conversation.setMode(Conversation.MODE_MULTI);
1118					conversation.setContactJid(jid);
1119				} else {
1120					conversation.setMode(Conversation.MODE_SINGLE);
1121					conversation.setContactJid(jid.toBareJid());
1122				}
1123				conversation.setNextEncryption(-1);
1124				conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1125				this.databaseBackend.updateConversation(conversation);
1126			} else {
1127				String conversationName;
1128				Contact contact = account.getRoster().getContact(jid);
1129				if (contact != null) {
1130					conversationName = contact.getDisplayName();
1131				} else {
1132					conversationName = jid.getLocalpart();
1133				}
1134				if (muc) {
1135					conversation = new Conversation(conversationName, account, jid,
1136							Conversation.MODE_MULTI);
1137				} else {
1138					conversation = new Conversation(conversationName, account, jid.toBareJid(),
1139							Conversation.MODE_SINGLE);
1140				}
1141				this.databaseBackend.createConversation(conversation);
1142			}
1143			if (account.getXmppConnection() != null
1144					&& account.getXmppConnection().getFeatures().mam()
1145					&& !muc) {
1146				if (query == null) {
1147					this.mMessageArchiveService.query(conversation);
1148				} else {
1149					if (query.getConversation() == null) {
1150						this.mMessageArchiveService.query(conversation, query.getStart());
1151					}
1152				}
1153			}
1154			checkDeletedFiles(conversation);
1155			this.conversations.add(conversation);
1156			updateConversationUi();
1157			return conversation;
1158		}
1159	}
1160
1161	public void archiveConversation(Conversation conversation) {
1162		getNotificationService().clear(conversation);
1163		conversation.setStatus(Conversation.STATUS_ARCHIVED);
1164		conversation.setNextEncryption(-1);
1165		synchronized (this.conversations) {
1166			if (conversation.getMode() == Conversation.MODE_MULTI) {
1167				if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1168					Bookmark bookmark = conversation.getBookmark();
1169					if (bookmark != null && bookmark.autojoin()) {
1170						bookmark.setAutojoin(false);
1171						pushBookmarks(bookmark.getAccount());
1172					}
1173				}
1174				leaveMuc(conversation);
1175			} else {
1176				conversation.endOtrIfNeeded();
1177			}
1178			this.databaseBackend.updateConversation(conversation);
1179			this.conversations.remove(conversation);
1180			updateConversationUi();
1181		}
1182	}
1183
1184	public void createAccount(final Account account) {
1185		account.initAccountServices(this);
1186		databaseBackend.createAccount(account);
1187		this.accounts.add(account);
1188		this.reconnectAccountInBackground(account);
1189		updateAccountUi();
1190	}
1191
1192	public void updateAccount(final Account account) {
1193		this.statusListener.onStatusChanged(account);
1194		databaseBackend.updateAccount(account);
1195		reconnectAccount(account, false);
1196		updateAccountUi();
1197		getNotificationService().updateErrorNotification();
1198	}
1199
1200	public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
1201		final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
1202		sendIqPacket(account, iq, new OnIqPacketReceived() {
1203			@Override
1204			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1205				if (packet.getType() == IqPacket.TYPE.RESULT) {
1206					account.setPassword(newPassword);
1207					databaseBackend.updateAccount(account);
1208					callback.onPasswordChangeSucceeded();
1209				} else {
1210					callback.onPasswordChangeFailed();
1211				}
1212			}
1213		});
1214	}
1215
1216	public void deleteAccount(final Account account) {
1217		synchronized (this.conversations) {
1218			for (final Conversation conversation : conversations) {
1219				if (conversation.getAccount() == account) {
1220					if (conversation.getMode() == Conversation.MODE_MULTI) {
1221						leaveMuc(conversation);
1222					} else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1223						conversation.endOtrIfNeeded();
1224					}
1225					conversations.remove(conversation);
1226				}
1227			}
1228			if (account.getXmppConnection() != null) {
1229				this.disconnect(account, true);
1230			}
1231			databaseBackend.deleteAccount(account);
1232			this.accounts.remove(account);
1233			updateAccountUi();
1234			getNotificationService().updateErrorNotification();
1235		}
1236	}
1237
1238	public void setOnConversationListChangedListener(OnConversationUpdate listener) {
1239		synchronized (this) {
1240			if (checkListeners()) {
1241				switchToForeground();
1242			}
1243			this.mOnConversationUpdate = listener;
1244			this.mNotificationService.setIsInForeground(true);
1245			if (this.convChangedListenerCount < 2) {
1246				this.convChangedListenerCount++;
1247			}
1248		}
1249	}
1250
1251	public void removeOnConversationListChangedListener() {
1252		synchronized (this) {
1253			this.convChangedListenerCount--;
1254			if (this.convChangedListenerCount <= 0) {
1255				this.convChangedListenerCount = 0;
1256				this.mOnConversationUpdate = null;
1257				this.mNotificationService.setIsInForeground(false);
1258				if (checkListeners()) {
1259					switchToBackground();
1260				}
1261			}
1262		}
1263	}
1264
1265	public void setOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
1266		synchronized (this) {
1267			if (checkListeners()) {
1268				switchToForeground();
1269			}
1270			this.mOnShowErrorToast = onShowErrorToast;
1271			if (this.showErrorToastListenerCount < 2) {
1272				this.showErrorToastListenerCount++;
1273			}
1274		}
1275		this.mOnShowErrorToast = onShowErrorToast;
1276	}
1277
1278	public void removeOnShowErrorToastListener() {
1279		synchronized (this) {
1280			this.showErrorToastListenerCount--;
1281			if (this.showErrorToastListenerCount <= 0) {
1282				this.showErrorToastListenerCount = 0;
1283				this.mOnShowErrorToast = null;
1284				if (checkListeners()) {
1285					switchToBackground();
1286				}
1287			}
1288		}
1289	}
1290
1291	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1292		synchronized (this) {
1293			if (checkListeners()) {
1294				switchToForeground();
1295			}
1296			this.mOnAccountUpdate = listener;
1297			if (this.accountChangedListenerCount < 2) {
1298				this.accountChangedListenerCount++;
1299			}
1300		}
1301	}
1302
1303	public void removeOnAccountListChangedListener() {
1304		synchronized (this) {
1305			this.accountChangedListenerCount--;
1306			if (this.accountChangedListenerCount <= 0) {
1307				this.mOnAccountUpdate = null;
1308				this.accountChangedListenerCount = 0;
1309				if (checkListeners()) {
1310					switchToBackground();
1311				}
1312			}
1313		}
1314	}
1315
1316	public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
1317		synchronized (this) {
1318			if (checkListeners()) {
1319				switchToForeground();
1320			}
1321			this.mOnRosterUpdate = listener;
1322			if (this.rosterChangedListenerCount < 2) {
1323				this.rosterChangedListenerCount++;
1324			}
1325		}
1326	}
1327
1328	public void removeOnRosterUpdateListener() {
1329		synchronized (this) {
1330			this.rosterChangedListenerCount--;
1331			if (this.rosterChangedListenerCount <= 0) {
1332				this.rosterChangedListenerCount = 0;
1333				this.mOnRosterUpdate = null;
1334				if (checkListeners()) {
1335					switchToBackground();
1336				}
1337			}
1338		}
1339	}
1340
1341	public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
1342		synchronized (this) {
1343			if (checkListeners()) {
1344				switchToForeground();
1345			}
1346			this.mOnUpdateBlocklist = listener;
1347			if (this.updateBlocklistListenerCount < 2) {
1348				this.updateBlocklistListenerCount++;
1349			}
1350		}
1351	}
1352
1353	public void removeOnUpdateBlocklistListener() {
1354		synchronized (this) {
1355			this.updateBlocklistListenerCount--;
1356			if (this.updateBlocklistListenerCount <= 0) {
1357				this.updateBlocklistListenerCount = 0;
1358				this.mOnUpdateBlocklist = null;
1359				if (checkListeners()) {
1360					switchToBackground();
1361				}
1362			}
1363		}
1364	}
1365
1366	public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
1367		synchronized (this) {
1368			if (checkListeners()) {
1369				switchToForeground();
1370			}
1371			this.mOnMucRosterUpdate = listener;
1372			if (this.mucRosterChangedListenerCount < 2) {
1373				this.mucRosterChangedListenerCount++;
1374			}
1375		}
1376	}
1377
1378	public void removeOnMucRosterUpdateListener() {
1379		synchronized (this) {
1380			this.mucRosterChangedListenerCount--;
1381			if (this.mucRosterChangedListenerCount <= 0) {
1382				this.mucRosterChangedListenerCount = 0;
1383				this.mOnMucRosterUpdate = null;
1384				if (checkListeners()) {
1385					switchToBackground();
1386				}
1387			}
1388		}
1389	}
1390
1391	private boolean checkListeners() {
1392		return (this.mOnAccountUpdate == null
1393				&& this.mOnConversationUpdate == null
1394				&& this.mOnRosterUpdate == null
1395				&& this.mOnUpdateBlocklist == null
1396				&& this.mOnShowErrorToast == null);
1397	}
1398
1399	private void switchToForeground() {
1400		for (Account account : getAccounts()) {
1401			if (account.getStatus() == Account.State.ONLINE) {
1402				XmppConnection connection = account.getXmppConnection();
1403				if (connection != null && connection.getFeatures().csi()) {
1404					connection.sendActive();
1405				}
1406			}
1407		}
1408		Log.d(Config.LOGTAG, "app switched into foreground");
1409	}
1410
1411	private void switchToBackground() {
1412		for (Account account : getAccounts()) {
1413			if (account.getStatus() == Account.State.ONLINE) {
1414				XmppConnection connection = account.getXmppConnection();
1415				if (connection != null && connection.getFeatures().csi()) {
1416					connection.sendInactive();
1417				}
1418			}
1419		}
1420		for(Conversation conversation : getConversations()) {
1421			conversation.setIncomingChatState(ChatState.ACTIVE);
1422		}
1423		this.mNotificationService.setIsInForeground(false);
1424		Log.d(Config.LOGTAG, "app switched into background");
1425	}
1426
1427	private void connectMultiModeConversations(Account account) {
1428		List<Conversation> conversations = getConversations();
1429		for (Conversation conversation : conversations) {
1430			if ((conversation.getMode() == Conversation.MODE_MULTI)
1431					&& (conversation.getAccount() == account)) {
1432				conversation.resetMucOptions();
1433				joinMuc(conversation);
1434			}
1435		}
1436	}
1437
1438	public void joinMuc(Conversation conversation) {
1439		Account account = conversation.getAccount();
1440		account.pendingConferenceJoins.remove(conversation);
1441		account.pendingConferenceLeaves.remove(conversation);
1442		if (account.getStatus() == Account.State.ONLINE) {
1443			final String nick = conversation.getMucOptions().getProposedNick();
1444			final Jid joinJid = conversation.getMucOptions().createJoinJid(nick);
1445			if (joinJid == null) {
1446				return; //safety net
1447			}
1448			Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
1449			PresencePacket packet = new PresencePacket();
1450			packet.setFrom(conversation.getAccount().getJid());
1451			packet.setTo(joinJid);
1452			Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
1453			if (conversation.getMucOptions().getPassword() != null) {
1454				x.addChild("password").setContent(conversation.getMucOptions().getPassword());
1455			}
1456			x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted()));
1457			String sig = account.getPgpSignature();
1458			if (sig != null) {
1459				packet.addChild("status").setContent("online");
1460				packet.addChild("x", "jabber:x:signed").setContent(sig);
1461			}
1462			sendPresencePacket(account, packet);
1463			fetchConferenceConfiguration(conversation);
1464			if (!joinJid.equals(conversation.getJid())) {
1465				conversation.setContactJid(joinJid);
1466				databaseBackend.updateConversation(conversation);
1467			}
1468			conversation.setHasMessagesLeftOnServer(false);
1469		} else {
1470			account.pendingConferenceJoins.add(conversation);
1471		}
1472	}
1473
1474	public void providePasswordForMuc(Conversation conversation, String password) {
1475		if (conversation.getMode() == Conversation.MODE_MULTI) {
1476			conversation.getMucOptions().setPassword(password);
1477			if (conversation.getBookmark() != null) {
1478				conversation.getBookmark().setAutojoin(true);
1479				pushBookmarks(conversation.getAccount());
1480			}
1481			databaseBackend.updateConversation(conversation);
1482			joinMuc(conversation);
1483		}
1484	}
1485
1486	public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
1487		final MucOptions options = conversation.getMucOptions();
1488		final Jid joinJid = options.createJoinJid(nick);
1489		if (options.online()) {
1490			Account account = conversation.getAccount();
1491			options.setOnRenameListener(new OnRenameListener() {
1492
1493				@Override
1494				public void onSuccess() {
1495					conversation.setContactJid(joinJid);
1496					databaseBackend.updateConversation(conversation);
1497					Bookmark bookmark = conversation.getBookmark();
1498					if (bookmark != null) {
1499						bookmark.setNick(nick);
1500						pushBookmarks(bookmark.getAccount());
1501					}
1502					callback.success(conversation);
1503				}
1504
1505				@Override
1506				public void onFailure() {
1507					callback.error(R.string.nick_in_use, conversation);
1508				}
1509			});
1510
1511			PresencePacket packet = new PresencePacket();
1512			packet.setTo(joinJid);
1513			packet.setFrom(conversation.getAccount().getJid());
1514
1515			String sig = account.getPgpSignature();
1516			if (sig != null) {
1517				packet.addChild("status").setContent("online");
1518				packet.addChild("x", "jabber:x:signed").setContent(sig);
1519			}
1520			sendPresencePacket(account, packet);
1521		} else {
1522			conversation.setContactJid(joinJid);
1523			databaseBackend.updateConversation(conversation);
1524			if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1525				Bookmark bookmark = conversation.getBookmark();
1526				if (bookmark != null) {
1527					bookmark.setNick(nick);
1528					pushBookmarks(bookmark.getAccount());
1529				}
1530				joinMuc(conversation);
1531			}
1532		}
1533	}
1534
1535	public void leaveMuc(Conversation conversation) {
1536		Account account = conversation.getAccount();
1537		account.pendingConferenceJoins.remove(conversation);
1538		account.pendingConferenceLeaves.remove(conversation);
1539		if (account.getStatus() == Account.State.ONLINE) {
1540			PresencePacket packet = new PresencePacket();
1541			packet.setTo(conversation.getJid());
1542			packet.setFrom(conversation.getAccount().getJid());
1543			packet.setAttribute("type", "unavailable");
1544			sendPresencePacket(conversation.getAccount(), packet);
1545			conversation.getMucOptions().setOffline();
1546			conversation.deregisterWithBookmark();
1547			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1548					+ ": leaving muc " + conversation.getJid());
1549		} else {
1550			account.pendingConferenceLeaves.add(conversation);
1551		}
1552	}
1553
1554	private String findConferenceServer(final Account account) {
1555		String server;
1556		if (account.getXmppConnection() != null) {
1557			server = account.getXmppConnection().getMucServer();
1558			if (server != null) {
1559				return server;
1560			}
1561		}
1562		for (Account other : getAccounts()) {
1563			if (other != account && other.getXmppConnection() != null) {
1564				server = other.getXmppConnection().getMucServer();
1565				if (server != null) {
1566					return server;
1567				}
1568			}
1569		}
1570		return null;
1571	}
1572
1573	public void createAdhocConference(final Account account, final Iterable<Jid> jids, final UiCallback<Conversation> callback) {
1574		Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString());
1575		if (account.getStatus() == Account.State.ONLINE) {
1576			try {
1577				String server = findConferenceServer(account);
1578				if (server == null) {
1579					if (callback != null) {
1580						callback.error(R.string.no_conference_server_found, null);
1581					}
1582					return;
1583				}
1584				String name = new BigInteger(75, getRNG()).toString(32);
1585				Jid jid = Jid.fromParts(name, server, null);
1586				final Conversation conversation = findOrCreateConversation(account, jid, true);
1587				joinMuc(conversation);
1588				Bundle options = new Bundle();
1589				options.putString("muc#roomconfig_persistentroom", "1");
1590				options.putString("muc#roomconfig_membersonly", "1");
1591				options.putString("muc#roomconfig_publicroom", "0");
1592				options.putString("muc#roomconfig_whois", "anyone");
1593				pushConferenceConfiguration(conversation, options, new OnConferenceOptionsPushed() {
1594					@Override
1595					public void onPushSucceeded() {
1596						for (Jid invite : jids) {
1597							invite(conversation, invite);
1598						}
1599						if (account.countPresences() > 1) {
1600							directInvite(conversation, account.getJid().toBareJid());
1601						}
1602						if (callback != null) {
1603							callback.success(conversation);
1604						}
1605					}
1606
1607					@Override
1608					public void onPushFailed() {
1609						if (callback != null) {
1610							callback.error(R.string.conference_creation_failed, conversation);
1611						}
1612					}
1613				});
1614
1615			} catch (InvalidJidException e) {
1616				if (callback != null) {
1617					callback.error(R.string.conference_creation_failed, null);
1618				}
1619			}
1620		} else {
1621			if (callback != null) {
1622				callback.error(R.string.not_connected_try_again, null);
1623			}
1624		}
1625	}
1626
1627	public void fetchConferenceConfiguration(final Conversation conversation) {
1628		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1629		request.setTo(conversation.getJid().toBareJid());
1630		request.query("http://jabber.org/protocol/disco#info");
1631		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
1632			@Override
1633			public void onIqPacketReceived(Account account, IqPacket packet) {
1634				if (packet.getType() != IqPacket.TYPE.ERROR) {
1635					ArrayList<String> features = new ArrayList<>();
1636					for (Element child : packet.query().getChildren()) {
1637						if (child != null && child.getName().equals("feature")) {
1638							String var = child.getAttribute("var");
1639							if (var != null) {
1640								features.add(var);
1641							}
1642						}
1643					}
1644					conversation.getMucOptions().updateFeatures(features);
1645					updateConversationUi();
1646				}
1647			}
1648		});
1649	}
1650
1651	public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) {
1652		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1653		request.setTo(conversation.getJid().toBareJid());
1654		request.query("http://jabber.org/protocol/muc#owner");
1655		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
1656			@Override
1657			public void onIqPacketReceived(Account account, IqPacket packet) {
1658				if (packet.getType() != IqPacket.TYPE.ERROR) {
1659					Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
1660					for (Field field : data.getFields()) {
1661						if (options.containsKey(field.getName())) {
1662							field.setValue(options.getString(field.getName()));
1663						}
1664					}
1665					data.submit();
1666					IqPacket set = new IqPacket(IqPacket.TYPE.SET);
1667					set.setTo(conversation.getJid().toBareJid());
1668					set.query("http://jabber.org/protocol/muc#owner").addChild(data);
1669					sendIqPacket(account, set, new OnIqPacketReceived() {
1670						@Override
1671						public void onIqPacketReceived(Account account, IqPacket packet) {
1672							if (packet.getType() == IqPacket.TYPE.RESULT) {
1673								if (callback != null) {
1674									callback.onPushSucceeded();
1675								}
1676							} else {
1677								if (callback != null) {
1678									callback.onPushFailed();
1679								}
1680							}
1681						}
1682					});
1683				} else {
1684					if (callback != null) {
1685						callback.onPushFailed();
1686					}
1687				}
1688			}
1689		});
1690	}
1691
1692	public void pushSubjectToConference(final Conversation conference, final String subject) {
1693		MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject);
1694		this.sendMessagePacket(conference.getAccount(), packet);
1695		final MucOptions mucOptions = conference.getMucOptions();
1696		final MucOptions.User self = mucOptions.getSelf();
1697		if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
1698			Bundle options = new Bundle();
1699			options.putString("muc#roomconfig_persistentroom", "1");
1700			this.pushConferenceConfiguration(conference, options, null);
1701		}
1702	}
1703
1704	public void changeAffiliationInConference(final Conversation conference, Jid user, MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
1705		final Jid jid = user.toBareJid();
1706		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
1707		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
1708			@Override
1709			public void onIqPacketReceived(Account account, IqPacket packet) {
1710				if (packet.getType() == IqPacket.TYPE.RESULT) {
1711					callback.onAffiliationChangedSuccessful(jid);
1712				} else {
1713					callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
1714				}
1715			}
1716		});
1717	}
1718
1719	public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
1720		List<Jid> jids = new ArrayList<>();
1721		for (MucOptions.User user : conference.getMucOptions().getUsers()) {
1722			if (user.getAffiliation() == before && user.getJid() != null) {
1723				jids.add(user.getJid());
1724			}
1725		}
1726		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
1727		sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
1728	}
1729
1730	public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
1731		IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
1732		Log.d(Config.LOGTAG, request.toString());
1733		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
1734			@Override
1735			public void onIqPacketReceived(Account account, IqPacket packet) {
1736				Log.d(Config.LOGTAG, packet.toString());
1737				if (packet.getType() == IqPacket.TYPE.RESULT) {
1738					callback.onRoleChangedSuccessful(nick);
1739				} else {
1740					callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
1741				}
1742			}
1743		});
1744	}
1745
1746	public void disconnect(Account account, boolean force) {
1747		if ((account.getStatus() == Account.State.ONLINE)
1748				|| (account.getStatus() == Account.State.DISABLED)) {
1749			if (!force) {
1750				List<Conversation> conversations = getConversations();
1751				for (Conversation conversation : conversations) {
1752					if (conversation.getAccount() == account) {
1753						if (conversation.getMode() == Conversation.MODE_MULTI) {
1754							leaveMuc(conversation);
1755						} else {
1756							if (conversation.endOtrIfNeeded()) {
1757								Log.d(Config.LOGTAG, account.getJid().toBareJid()
1758										+ ": ended otr session with "
1759										+ conversation.getJid());
1760							}
1761						}
1762					}
1763				}
1764				sendOfflinePresence(account);
1765			}
1766			account.getXmppConnection().disconnect(force);
1767		}
1768	}
1769
1770	@Override
1771	public IBinder onBind(Intent intent) {
1772		return mBinder;
1773	}
1774
1775	public void updateMessage(Message message) {
1776		databaseBackend.updateMessage(message);
1777		updateConversationUi();
1778	}
1779
1780	protected void syncDirtyContacts(Account account) {
1781		for (Contact contact : account.getRoster().getContacts()) {
1782			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1783				pushContactToServer(contact);
1784			}
1785			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1786				deleteContactOnServer(contact);
1787			}
1788		}
1789	}
1790
1791	public void createContact(Contact contact) {
1792		SharedPreferences sharedPref = getPreferences();
1793		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1794		if (autoGrant) {
1795			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1796			contact.setOption(Contact.Options.ASKING);
1797		}
1798		pushContactToServer(contact);
1799	}
1800
1801	public void onOtrSessionEstablished(Conversation conversation) {
1802		final Account account = conversation.getAccount();
1803		final Session otrSession = conversation.getOtrSession();
1804		Log.d(Config.LOGTAG,
1805				account.getJid().toBareJid() + " otr session established with "
1806						+ conversation.getJid() + "/"
1807						+ otrSession.getSessionID().getUserID());
1808		conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
1809
1810			@Override
1811			public void onMessageFound(Message message) {
1812				SessionID id = otrSession.getSessionID();
1813				try {
1814					message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
1815				} catch (InvalidJidException e) {
1816					return;
1817				}
1818				if (message.needsUploading()) {
1819					mJingleConnectionManager.createNewConnection(message);
1820				} else {
1821					MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true);
1822					if (outPacket != null) {
1823						message.setStatus(Message.STATUS_SEND);
1824						databaseBackend.updateMessage(message);
1825						sendMessagePacket(account, outPacket);
1826					}
1827				}
1828				updateConversationUi();
1829			}
1830		});
1831	}
1832
1833	public boolean renewSymmetricKey(Conversation conversation) {
1834		Account account = conversation.getAccount();
1835		byte[] symmetricKey = new byte[32];
1836		this.mRandom.nextBytes(symmetricKey);
1837		Session otrSession = conversation.getOtrSession();
1838		if (otrSession != null) {
1839			MessagePacket packet = new MessagePacket();
1840			packet.setType(MessagePacket.TYPE_CHAT);
1841			packet.setFrom(account.getJid());
1842			packet.addChild("private", "urn:xmpp:carbons:2");
1843			packet.addChild("no-copy", "urn:xmpp:hints");
1844			packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
1845					+ otrSession.getSessionID().getUserID());
1846			try {
1847				packet.setBody(otrSession
1848						.transformSending(CryptoHelper.FILETRANSFER
1849								+ CryptoHelper.bytesToHex(symmetricKey))[0]);
1850				sendMessagePacket(account, packet);
1851				conversation.setSymmetricKey(symmetricKey);
1852				return true;
1853			} catch (OtrException e) {
1854				return false;
1855			}
1856		}
1857		return false;
1858	}
1859
1860	public void pushContactToServer(final Contact contact) {
1861		contact.resetOption(Contact.Options.DIRTY_DELETE);
1862		contact.setOption(Contact.Options.DIRTY_PUSH);
1863		final Account account = contact.getAccount();
1864		if (account.getStatus() == Account.State.ONLINE) {
1865			final boolean ask = contact.getOption(Contact.Options.ASKING);
1866			final boolean sendUpdates = contact
1867					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
1868					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
1869			final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1870			iq.query(Xmlns.ROSTER).addChild(contact.asElement());
1871			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
1872			if (sendUpdates) {
1873				sendPresencePacket(account,
1874						mPresenceGenerator.sendPresenceUpdatesTo(contact));
1875			}
1876			if (ask) {
1877				sendPresencePacket(account,
1878						mPresenceGenerator.requestPresenceUpdatesFrom(contact));
1879			}
1880		}
1881	}
1882
1883	public void publishAvatar(final Account account,
1884							  final Uri image,
1885							  final UiCallback<Avatar> callback) {
1886		final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
1887		final int size = Config.AVATAR_SIZE;
1888		final Avatar avatar = getFileBackend()
1889				.getPepAvatar(image, size, format);
1890		if (avatar != null) {
1891			avatar.height = size;
1892			avatar.width = size;
1893			if (format.equals(Bitmap.CompressFormat.WEBP)) {
1894				avatar.type = "image/webp";
1895			} else if (format.equals(Bitmap.CompressFormat.JPEG)) {
1896				avatar.type = "image/jpeg";
1897			} else if (format.equals(Bitmap.CompressFormat.PNG)) {
1898				avatar.type = "image/png";
1899			}
1900			if (!getFileBackend().save(avatar)) {
1901				callback.error(R.string.error_saving_avatar, avatar);
1902				return;
1903			}
1904			final IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
1905			this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1906
1907				@Override
1908				public void onIqPacketReceived(Account account, IqPacket result) {
1909					if (result.getType() == IqPacket.TYPE.RESULT) {
1910						final IqPacket packet = XmppConnectionService.this.mIqGenerator
1911								.publishAvatarMetadata(avatar);
1912						sendIqPacket(account, packet, new OnIqPacketReceived() {
1913
1914							@Override
1915							public void onIqPacketReceived(Account account,
1916														   IqPacket result) {
1917								if (result.getType() == IqPacket.TYPE.RESULT) {
1918									if (account.setAvatar(avatar.getFilename())) {
1919										getAvatarService().clear(account);
1920										databaseBackend.updateAccount(account);
1921									}
1922									callback.success(avatar);
1923								} else {
1924									callback.error(
1925											R.string.error_publish_avatar_server_reject,
1926											avatar);
1927								}
1928							}
1929						});
1930					} else {
1931						callback.error(
1932								R.string.error_publish_avatar_server_reject,
1933								avatar);
1934					}
1935				}
1936			});
1937		} else {
1938			callback.error(R.string.error_publish_avatar_converting, null);
1939		}
1940	}
1941
1942	public void fetchAvatar(Account account, Avatar avatar) {
1943		fetchAvatar(account, avatar, null);
1944	}
1945
1946	private static String generateFetchKey(Account account, final Avatar avatar) {
1947		return account.getJid().toBareJid()+"_"+avatar.owner+"_"+avatar.sha1sum;
1948	}
1949
1950	public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
1951		final String KEY = generateFetchKey(account, avatar);
1952		synchronized(this.mInProgressAvatarFetches) {
1953			if (this.mInProgressAvatarFetches.contains(KEY)) {
1954				return;
1955			} else {
1956				switch (avatar.origin) {
1957					case PEP:
1958						this.mInProgressAvatarFetches.add(KEY);
1959						fetchAvatarPep(account, avatar, callback);
1960						break;
1961					case VCARD:
1962						this.mInProgressAvatarFetches.add(KEY);
1963						fetchAvatarVcard(account, avatar, callback);
1964						break;
1965				}
1966			}
1967		}
1968	}
1969
1970	private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
1971		IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
1972		sendIqPacket(account, packet, new OnIqPacketReceived() {
1973
1974			@Override
1975			public void onIqPacketReceived(Account account, IqPacket result) {
1976				synchronized (mInProgressAvatarFetches) {
1977					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
1978				}
1979				final String ERROR = account.getJid().toBareJid()
1980						+ ": fetching avatar for " + avatar.owner + " failed ";
1981				if (result.getType() == IqPacket.TYPE.RESULT) {
1982					avatar.image = mIqParser.avatarData(result);
1983					if (avatar.image != null) {
1984						if (getFileBackend().save(avatar)) {
1985							if (account.getJid().toBareJid().equals(avatar.owner)) {
1986								if (account.setAvatar(avatar.getFilename())) {
1987									databaseBackend.updateAccount(account);
1988								}
1989								getAvatarService().clear(account);
1990								updateConversationUi();
1991								updateAccountUi();
1992							} else {
1993								Contact contact = account.getRoster()
1994										.getContact(avatar.owner);
1995								contact.setAvatar(avatar);
1996								getAvatarService().clear(contact);
1997								updateConversationUi();
1998								updateRosterUi();
1999							}
2000							if (callback != null) {
2001								callback.success(avatar);
2002							}
2003							Log.d(Config.LOGTAG, account.getJid().toBareJid()
2004									+ ": succesfuly fetched pep avatar for " + avatar.owner);
2005							return;
2006						}
2007					} else {
2008
2009						Log.d(Config.LOGTAG, ERROR + "(parsing error)");
2010					}
2011				} else {
2012					Element error = result.findChild("error");
2013					if (error == null) {
2014						Log.d(Config.LOGTAG, ERROR + "(server error)");
2015					} else {
2016						Log.d(Config.LOGTAG, ERROR + error.toString());
2017					}
2018				}
2019				if (callback != null) {
2020					callback.error(0, null);
2021				}
2022
2023			}
2024		});
2025	}
2026
2027	private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2028		IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
2029		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2030			@Override
2031			public void onIqPacketReceived(Account account, IqPacket packet) {
2032				synchronized (mInProgressAvatarFetches) {
2033					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2034				}
2035				if (packet.getType() == IqPacket.TYPE.RESULT) {
2036					Element vCard = packet.findChild("vCard", "vcard-temp");
2037					Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
2038					String image = photo != null ? photo.findChildContent("BINVAL") : null;
2039					if (image != null) {
2040						avatar.image = image;
2041						if (getFileBackend().save(avatar)) {
2042							Log.d(Config.LOGTAG, account.getJid().toBareJid()
2043									+ ": successfully fetched vCard avatar for " + avatar.owner);
2044							Contact contact = account.getRoster()
2045									.getContact(avatar.owner);
2046							contact.setAvatar(avatar);
2047							getAvatarService().clear(contact);
2048							updateConversationUi();
2049							updateRosterUi();
2050						}
2051					}
2052				}
2053			}
2054		});
2055	}
2056
2057	public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
2058		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
2059		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2060
2061			@Override
2062			public void onIqPacketReceived(Account account, IqPacket packet) {
2063				if (packet.getType() == IqPacket.TYPE.RESULT) {
2064					Element pubsub = packet.findChild("pubsub",
2065							"http://jabber.org/protocol/pubsub");
2066					if (pubsub != null) {
2067						Element items = pubsub.findChild("items");
2068						if (items != null) {
2069							Avatar avatar = Avatar.parseMetadata(items);
2070							if (avatar != null) {
2071								avatar.owner = account.getJid().toBareJid();
2072								if (fileBackend.isAvatarCached(avatar)) {
2073									if (account.setAvatar(avatar.getFilename())) {
2074										databaseBackend.updateAccount(account);
2075									}
2076									getAvatarService().clear(account);
2077									callback.success(avatar);
2078								} else {
2079									fetchAvatarPep(account, avatar, callback);
2080								}
2081								return;
2082							}
2083						}
2084					}
2085				}
2086				callback.error(0, null);
2087			}
2088		});
2089	}
2090
2091	public void deleteContactOnServer(Contact contact) {
2092		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
2093		contact.resetOption(Contact.Options.DIRTY_PUSH);
2094		contact.setOption(Contact.Options.DIRTY_DELETE);
2095		Account account = contact.getAccount();
2096		if (account.getStatus() == Account.State.ONLINE) {
2097			IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2098			Element item = iq.query(Xmlns.ROSTER).addChild("item");
2099			item.setAttribute("jid", contact.getJid().toString());
2100			item.setAttribute("subscription", "remove");
2101			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2102		}
2103	}
2104
2105	public void updateConversation(Conversation conversation) {
2106		this.databaseBackend.updateConversation(conversation);
2107	}
2108
2109	public void reconnectAccount(final Account account, final boolean force) {
2110		synchronized (account) {
2111			if (account.getXmppConnection() != null) {
2112				disconnect(account, force);
2113			}
2114			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2115
2116				synchronized (this.mInProgressAvatarFetches) {
2117					for(Iterator<String> iterator = this.mInProgressAvatarFetches.iterator(); iterator.hasNext();) {
2118						final String KEY = iterator.next();
2119						if (KEY.startsWith(account.getJid().toBareJid()+"_")) {
2120							iterator.remove();
2121						}
2122					}
2123				}
2124
2125				if (account.getXmppConnection() == null) {
2126					account.setXmppConnection(createConnection(account));
2127				}
2128				Thread thread = new Thread(account.getXmppConnection());
2129				thread.start();
2130				scheduleWakeUpCall(Config.CONNECT_TIMEOUT, account.getUuid().hashCode());
2131			} else {
2132				account.getRoster().clearPresences();
2133				account.setXmppConnection(null);
2134			}
2135		}
2136	}
2137
2138	public void reconnectAccountInBackground(final Account account) {
2139		new Thread(new Runnable() {
2140			@Override
2141			public void run() {
2142				reconnectAccount(account,false);
2143			}
2144		}).start();
2145	}
2146
2147	public void invite(Conversation conversation, Jid contact) {
2148		Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+": inviting "+contact+" to "+conversation.getJid().toBareJid());
2149		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
2150		sendMessagePacket(conversation.getAccount(), packet);
2151	}
2152
2153	public void directInvite(Conversation conversation, Jid jid) {
2154		MessagePacket packet = mMessageGenerator.directInvite(conversation, jid);
2155		sendMessagePacket(conversation.getAccount(),packet);
2156	}
2157
2158	public void resetSendingToWaiting(Account account) {
2159		for (Conversation conversation : getConversations()) {
2160			if (conversation.getAccount() == account) {
2161				conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
2162
2163					@Override
2164					public void onMessageFound(Message message) {
2165						markMessage(message, Message.STATUS_WAITING);
2166					}
2167				});
2168			}
2169		}
2170	}
2171
2172	public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
2173		if (uuid == null) {
2174			return null;
2175		}
2176		for (Conversation conversation : getConversations()) {
2177			if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) {
2178				final Message message = conversation.findSentMessageWithUuid(uuid);
2179				if (message != null) {
2180					markMessage(message, status);
2181				}
2182				return message;
2183			}
2184		}
2185		return null;
2186	}
2187
2188	public boolean markMessage(Conversation conversation, String uuid,
2189							   int status) {
2190		if (uuid == null) {
2191			return false;
2192		} else {
2193			Message message = conversation.findSentMessageWithUuid(uuid);
2194			if (message != null) {
2195				markMessage(message, status);
2196				return true;
2197			} else {
2198				return false;
2199			}
2200		}
2201	}
2202
2203	public void markMessage(Message message, int status) {
2204		if (status == Message.STATUS_SEND_FAILED
2205				&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
2206				.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
2207			return;
2208		}
2209		message.setStatus(status);
2210		databaseBackend.updateMessage(message);
2211		updateConversationUi();
2212	}
2213
2214	public SharedPreferences getPreferences() {
2215		return PreferenceManager
2216				.getDefaultSharedPreferences(getApplicationContext());
2217	}
2218
2219	public boolean forceEncryption() {
2220		return getPreferences().getBoolean("force_encryption", false);
2221	}
2222
2223	public boolean confirmMessages() {
2224		return getPreferences().getBoolean("confirm_messages", true);
2225	}
2226
2227	public boolean sendChatStates() {
2228		return getPreferences().getBoolean("chat_states", false);
2229	}
2230
2231	public boolean saveEncryptedMessages() {
2232		return !getPreferences().getBoolean("dont_save_encrypted", false);
2233	}
2234
2235	public boolean indicateReceived() {
2236		return getPreferences().getBoolean("indicate_received", false);
2237	}
2238
2239	public int unreadCount() {
2240		int count = 0;
2241		for(Conversation conversation : getConversations()) {
2242			count += conversation.unreadCount();
2243		}
2244		return count;
2245	}
2246
2247
2248	public void showErrorToastInUi(int resId) {
2249		if (mOnShowErrorToast != null) {
2250			mOnShowErrorToast.onShowErrorToast(resId);
2251		}
2252	}
2253
2254	public void updateConversationUi() {
2255		if (mOnConversationUpdate != null) {
2256			mOnConversationUpdate.onConversationUpdate();
2257		}
2258	}
2259
2260	public void updateAccountUi() {
2261		if (mOnAccountUpdate != null) {
2262			mOnAccountUpdate.onAccountUpdate();
2263		}
2264	}
2265
2266	public void updateRosterUi() {
2267		if (mOnRosterUpdate != null) {
2268			mOnRosterUpdate.onRosterUpdate();
2269		}
2270	}
2271
2272	public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
2273		if (mOnUpdateBlocklist != null) {
2274			mOnUpdateBlocklist.OnUpdateBlocklist(status);
2275		}
2276	}
2277
2278	public void updateMucRosterUi() {
2279		if (mOnMucRosterUpdate != null) {
2280			mOnMucRosterUpdate.onMucRosterUpdate();
2281		}
2282	}
2283
2284	public Account findAccountByJid(final Jid accountJid) {
2285		for (Account account : this.accounts) {
2286			if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
2287				return account;
2288			}
2289		}
2290		return null;
2291	}
2292
2293	public Conversation findConversationByUuid(String uuid) {
2294		for (Conversation conversation : getConversations()) {
2295			if (conversation.getUuid().equals(uuid)) {
2296				return conversation;
2297			}
2298		}
2299		return null;
2300	}
2301
2302	public void markRead(final Conversation conversation) {
2303		mNotificationService.clear(conversation);
2304		conversation.markRead();
2305		updateUnreadCountBadge();
2306	}
2307
2308	public synchronized void updateUnreadCountBadge() {
2309		int count = unreadCount();
2310		if (unreadCount != count) {
2311			Log.d(Config.LOGTAG, "update unread count to " + count);
2312			if (count > 0) {
2313				ShortcutBadger.with(getApplicationContext()).count(count);
2314			} else {
2315				ShortcutBadger.with(getApplicationContext()).remove();
2316			}
2317			unreadCount = count;
2318		}
2319	}
2320
2321	public void sendReadMarker(final Conversation conversation) {
2322		final Message markable = conversation.getLatestMarkableMessage();
2323		this.markRead(conversation);
2324		if (confirmMessages() && markable != null && markable.getRemoteMsgId() != null) {
2325			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
2326			Account account = conversation.getAccount();
2327			final Jid to = markable.getCounterpart();
2328			MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
2329			this.sendMessagePacket(conversation.getAccount(), packet);
2330		}
2331		updateConversationUi();
2332	}
2333
2334	public SecureRandom getRNG() {
2335		return this.mRandom;
2336	}
2337
2338	public MemorizingTrustManager getMemorizingTrustManager() {
2339		return this.mMemorizingTrustManager;
2340	}
2341
2342	public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
2343		this.mMemorizingTrustManager = trustManager;
2344	}
2345
2346	public void updateMemorizingTrustmanager() {
2347		final MemorizingTrustManager tm;
2348		final boolean dontTrustSystemCAs = getPreferences().getBoolean("dont_trust_system_cas", false);
2349		if (dontTrustSystemCAs) {
2350			 tm = new MemorizingTrustManager(getApplicationContext(), null);
2351		} else {
2352			tm = new MemorizingTrustManager(getApplicationContext());
2353		}
2354		setMemorizingTrustManager(tm);
2355	}
2356
2357	public PowerManager getPowerManager() {
2358		return this.pm;
2359	}
2360
2361	public LruCache<String, Bitmap> getBitmapCache() {
2362		return this.mBitmapCache;
2363	}
2364
2365	public void syncRosterToDisk(final Account account) {
2366		Runnable runnable = new Runnable() {
2367
2368			@Override
2369			public void run() {
2370				databaseBackend.writeRoster(account.getRoster());
2371			}
2372		};
2373		mDatabaseExecutor.execute(runnable);
2374
2375	}
2376
2377	public List<String> getKnownHosts() {
2378		final List<String> hosts = new ArrayList<>();
2379		for (final Account account : getAccounts()) {
2380			if (!hosts.contains(account.getServer().toString())) {
2381				hosts.add(account.getServer().toString());
2382			}
2383			for (final Contact contact : account.getRoster().getContacts()) {
2384				if (contact.showInRoster()) {
2385					final String server = contact.getServer().toString();
2386					if (server != null && !hosts.contains(server)) {
2387						hosts.add(server);
2388					}
2389				}
2390			}
2391		}
2392		return hosts;
2393	}
2394
2395	public List<String> getKnownConferenceHosts() {
2396		final ArrayList<String> mucServers = new ArrayList<>();
2397		for (final Account account : accounts) {
2398			if (account.getXmppConnection() != null) {
2399				final String server = account.getXmppConnection().getMucServer();
2400				if (server != null && !mucServers.contains(server)) {
2401					mucServers.add(server);
2402				}
2403			}
2404		}
2405		return mucServers;
2406	}
2407
2408	public void sendMessagePacket(Account account, MessagePacket packet) {
2409		XmppConnection connection = account.getXmppConnection();
2410		if (connection != null) {
2411			connection.sendMessagePacket(packet);
2412		}
2413	}
2414
2415	public void sendPresencePacket(Account account, PresencePacket packet) {
2416		XmppConnection connection = account.getXmppConnection();
2417		if (connection != null) {
2418			connection.sendPresencePacket(packet);
2419		}
2420	}
2421
2422	public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
2423		final XmppConnection connection = account.getXmppConnection();
2424		if (connection != null) {
2425			connection.sendIqPacket(packet, callback);
2426		}
2427	}
2428
2429	public void sendPresence(final Account account) {
2430		sendPresencePacket(account, mPresenceGenerator.sendPresence(account));
2431	}
2432
2433	public void sendOfflinePresence(final Account account) {
2434		sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
2435	}
2436
2437	public MessageGenerator getMessageGenerator() {
2438		return this.mMessageGenerator;
2439	}
2440
2441	public PresenceGenerator getPresenceGenerator() {
2442		return this.mPresenceGenerator;
2443	}
2444
2445	public IqGenerator getIqGenerator() {
2446		return this.mIqGenerator;
2447	}
2448
2449	public IqParser getIqParser() {
2450		return this.mIqParser;
2451	}
2452
2453	public JingleConnectionManager getJingleConnectionManager() {
2454		return this.mJingleConnectionManager;
2455	}
2456
2457	public MessageArchiveService getMessageArchiveService() {
2458		return this.mMessageArchiveService;
2459	}
2460
2461	public List<Contact> findContacts(Jid jid) {
2462		ArrayList<Contact> contacts = new ArrayList<>();
2463		for (Account account : getAccounts()) {
2464			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2465				Contact contact = account.getRoster().getContactFromRoster(jid);
2466				if (contact != null) {
2467					contacts.add(contact);
2468				}
2469			}
2470		}
2471		return contacts;
2472	}
2473
2474	public NotificationService getNotificationService() {
2475		return this.mNotificationService;
2476	}
2477
2478	public HttpConnectionManager getHttpConnectionManager() {
2479		return this.mHttpConnectionManager;
2480	}
2481
2482	public void resendFailedMessages(final Message message) {
2483		final Collection<Message> messages = new ArrayList<>();
2484		Message current = message;
2485		while (current.getStatus() == Message.STATUS_SEND_FAILED) {
2486			messages.add(current);
2487			if (current.mergeable(current.next())) {
2488				current = current.next();
2489			} else {
2490				break;
2491			}
2492		}
2493		for (final Message msg : messages) {
2494			markMessage(msg, Message.STATUS_WAITING);
2495			this.resendMessage(msg);
2496		}
2497	}
2498
2499	public void clearConversationHistory(final Conversation conversation) {
2500		conversation.clearMessages();
2501		conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
2502		new Thread(new Runnable() {
2503			@Override
2504			public void run() {
2505				databaseBackend.deleteMessagesInConversation(conversation);
2506			}
2507		}).start();
2508	}
2509
2510	public void sendBlockRequest(final Blockable blockable) {
2511		if (blockable != null && blockable.getBlockedJid() != null) {
2512			final Jid jid = blockable.getBlockedJid();
2513			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() {
2514
2515				@Override
2516				public void onIqPacketReceived(final Account account, final IqPacket packet) {
2517					if (packet.getType() == IqPacket.TYPE.RESULT) {
2518						account.getBlocklist().add(jid);
2519						updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
2520					}
2521				}
2522			});
2523		}
2524	}
2525
2526	public void sendUnblockRequest(final Blockable blockable) {
2527		if (blockable != null && blockable.getJid() != null) {
2528			final Jid jid = blockable.getBlockedJid();
2529			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
2530				@Override
2531				public void onIqPacketReceived(final Account account, final IqPacket packet) {
2532					if (packet.getType() == IqPacket.TYPE.RESULT) {
2533						account.getBlocklist().remove(jid);
2534						updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
2535					}
2536				}
2537			});
2538		}
2539	}
2540
2541	public interface OnMoreMessagesLoaded {
2542		public void onMoreMessagesLoaded(int count, Conversation conversation);
2543
2544		public void informUser(int r);
2545	}
2546
2547	public interface OnAccountPasswordChanged {
2548		public void onPasswordChangeSucceeded();
2549
2550		public void onPasswordChangeFailed();
2551	}
2552
2553	public interface OnAffiliationChanged {
2554		public void onAffiliationChangedSuccessful(Jid jid);
2555
2556		public void onAffiliationChangeFailed(Jid jid, int resId);
2557	}
2558
2559	public interface OnRoleChanged {
2560		public void onRoleChangedSuccessful(String nick);
2561
2562		public void onRoleChangeFailed(String nick, int resid);
2563	}
2564
2565	public interface OnConversationUpdate {
2566		public void onConversationUpdate();
2567	}
2568
2569	public interface OnAccountUpdate {
2570		public void onAccountUpdate();
2571	}
2572
2573	public interface OnRosterUpdate {
2574		public void onRosterUpdate();
2575	}
2576
2577	public interface OnMucRosterUpdate {
2578		public void onMucRosterUpdate();
2579	}
2580
2581	public interface OnConferenceOptionsPushed {
2582		public void onPushSucceeded();
2583
2584		public void onPushFailed();
2585	}
2586
2587	public interface OnShowErrorToast {
2588		void onShowErrorToast(int resId);
2589	}
2590
2591	public class XmppConnectionBinder extends Binder {
2592		public XmppConnectionService getService() {
2593			return XmppConnectionService.this;
2594		}
2595	}
2596}