XmppConnectionService.java

   1package eu.siacs.conversations.services;
   2
   3import android.annotation.SuppressLint;
   4import android.annotation.TargetApi;
   5import android.app.AlarmManager;
   6import android.app.PendingIntent;
   7import android.app.Service;
   8import android.content.Context;
   9import android.content.Intent;
  10import android.content.IntentFilter;
  11import android.content.SharedPreferences;
  12import android.database.ContentObserver;
  13import android.graphics.Bitmap;
  14import android.media.AudioManager;
  15import android.net.ConnectivityManager;
  16import android.net.NetworkInfo;
  17import android.net.Uri;
  18import android.os.Binder;
  19import android.os.Build;
  20import android.os.Bundle;
  21import android.os.Environment;
  22import android.os.IBinder;
  23import android.os.PowerManager;
  24import android.os.PowerManager.WakeLock;
  25import android.os.SystemClock;
  26import android.preference.PreferenceManager;
  27import android.provider.ContactsContract;
  28import android.security.KeyChain;
  29import android.support.v4.app.RemoteInput;
  30import android.util.DisplayMetrics;
  31import android.util.Log;
  32import android.util.LruCache;
  33import android.util.Pair;
  34
  35import net.java.otr4j.OtrException;
  36import net.java.otr4j.session.Session;
  37import net.java.otr4j.session.SessionID;
  38import net.java.otr4j.session.SessionImpl;
  39import net.java.otr4j.session.SessionStatus;
  40
  41import org.openintents.openpgp.IOpenPgpService2;
  42import org.openintents.openpgp.util.OpenPgpApi;
  43import org.openintents.openpgp.util.OpenPgpServiceConnection;
  44
  45import java.math.BigInteger;
  46import java.security.SecureRandom;
  47import java.security.cert.CertificateException;
  48import java.security.cert.X509Certificate;
  49import java.util.ArrayList;
  50import java.util.Arrays;
  51import java.util.Collection;
  52import java.util.Collections;
  53import java.util.HashMap;
  54import java.util.HashSet;
  55import java.util.Hashtable;
  56import java.util.Iterator;
  57import java.util.List;
  58import java.util.ListIterator;
  59import java.util.Locale;
  60import java.util.Map;
  61import java.util.concurrent.CopyOnWriteArrayList;
  62
  63import de.duenndns.ssl.MemorizingTrustManager;
  64import eu.siacs.conversations.Config;
  65import eu.siacs.conversations.R;
  66import eu.siacs.conversations.crypto.PgpDecryptionService;
  67import eu.siacs.conversations.crypto.PgpEngine;
  68import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  69import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  70import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
  71import eu.siacs.conversations.entities.Account;
  72import eu.siacs.conversations.entities.Blockable;
  73import eu.siacs.conversations.entities.Bookmark;
  74import eu.siacs.conversations.entities.Contact;
  75import eu.siacs.conversations.entities.Conversation;
  76import eu.siacs.conversations.entities.DownloadableFile;
  77import eu.siacs.conversations.entities.Message;
  78import eu.siacs.conversations.entities.MucOptions;
  79import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
  80import eu.siacs.conversations.entities.Presence;
  81import eu.siacs.conversations.entities.PresenceTemplate;
  82import eu.siacs.conversations.entities.Roster;
  83import eu.siacs.conversations.entities.ServiceDiscoveryResult;
  84import eu.siacs.conversations.entities.Transferable;
  85import eu.siacs.conversations.entities.TransferablePlaceholder;
  86import eu.siacs.conversations.generator.AbstractGenerator;
  87import eu.siacs.conversations.generator.IqGenerator;
  88import eu.siacs.conversations.generator.MessageGenerator;
  89import eu.siacs.conversations.generator.PresenceGenerator;
  90import eu.siacs.conversations.http.HttpConnectionManager;
  91import eu.siacs.conversations.parser.AbstractParser;
  92import eu.siacs.conversations.parser.IqParser;
  93import eu.siacs.conversations.parser.MessageParser;
  94import eu.siacs.conversations.parser.PresenceParser;
  95import eu.siacs.conversations.persistance.DatabaseBackend;
  96import eu.siacs.conversations.persistance.FileBackend;
  97import eu.siacs.conversations.ui.SettingsActivity;
  98import eu.siacs.conversations.ui.UiCallback;
  99import eu.siacs.conversations.utils.ConversationsFileObserver;
 100import eu.siacs.conversations.utils.CryptoHelper;
 101import eu.siacs.conversations.utils.ExceptionHelper;
 102import eu.siacs.conversations.utils.MimeUtils;
 103import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
 104import eu.siacs.conversations.utils.PRNGFixes;
 105import eu.siacs.conversations.utils.PhoneHelper;
 106import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
 107import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
 108import eu.siacs.conversations.utils.Xmlns;
 109import eu.siacs.conversations.utils.XmppUri;
 110import eu.siacs.conversations.xml.Element;
 111import eu.siacs.conversations.xmpp.OnBindListener;
 112import eu.siacs.conversations.xmpp.OnContactStatusChanged;
 113import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 114import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 115import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
 116import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
 117import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
 118import eu.siacs.conversations.xmpp.OnStatusChanged;
 119import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 120import eu.siacs.conversations.xmpp.XmppConnection;
 121import eu.siacs.conversations.xmpp.chatstate.ChatState;
 122import eu.siacs.conversations.xmpp.forms.Data;
 123import eu.siacs.conversations.xmpp.forms.Field;
 124import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 125import eu.siacs.conversations.xmpp.jid.Jid;
 126import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 127import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
 128import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 129import eu.siacs.conversations.xmpp.pep.Avatar;
 130import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 131import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 132import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 133import me.leolin.shortcutbadger.ShortcutBadger;
 134
 135public class XmppConnectionService extends Service {
 136
 137	public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations";
 138	public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification";
 139	public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground";
 140	public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error";
 141	public static final String ACTION_TRY_AGAIN = "try_again";
 142	public static final String ACTION_IDLE_PING = "idle_ping";
 143	private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
 144	public static final String ACTION_GCM_TOKEN_REFRESH = "gcm_token_refresh";
 145	public static final String ACTION_GCM_MESSAGE_RECEIVED = "gcm_message_received";
 146	private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor();
 147	private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor();
 148	private ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor(true);
 149	private final IBinder mBinder = new XmppConnectionBinder();
 150	private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
 151	private final IqGenerator mIqGenerator = new IqGenerator(this);
 152	private final List<String> mInProgressAvatarFetches = new ArrayList<>();
 153	private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
 154
 155	private long mLastActivity = 0;
 156
 157	public DatabaseBackend databaseBackend;
 158	private ContentObserver contactObserver = new ContentObserver(null) {
 159		@Override
 160		public void onChange(boolean selfChange) {
 161			super.onChange(selfChange);
 162			Intent intent = new Intent(getApplicationContext(),
 163					XmppConnectionService.class);
 164			intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
 165			startService(intent);
 166		}
 167	};
 168	private FileBackend fileBackend = new FileBackend(this);
 169	private MemorizingTrustManager mMemorizingTrustManager;
 170	private NotificationService mNotificationService = new NotificationService(
 171			this);
 172	private OnMessagePacketReceived mMessageParser = new MessageParser(this);
 173	private OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
 174	private IqParser mIqParser = new IqParser(this);
 175	private OnIqPacketReceived mDefaultIqHandler = new OnIqPacketReceived() {
 176		@Override
 177		public void onIqPacketReceived(Account account, IqPacket packet) {
 178			if (packet.getType() != IqPacket.TYPE.RESULT) {
 179				Element error = packet.findChild("error");
 180				String text = error != null ? error.findChildContent("text") : null;
 181				if (text != null) {
 182					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": received iq error - " + text);
 183				}
 184			}
 185		}
 186	};
 187	private MessageGenerator mMessageGenerator = new MessageGenerator(this);
 188	private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
 189	private List<Account> accounts;
 190	private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
 191			this);
 192	public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
 193
 194		@Override
 195		public void onContactStatusChanged(Contact contact, boolean online) {
 196			Conversation conversation = find(getConversations(), contact);
 197			if (conversation != null) {
 198				if (online) {
 199					conversation.endOtrIfNeeded();
 200					if (contact.getPresences().size() == 1) {
 201						sendUnsentMessages(conversation);
 202					}
 203				} else {
 204					//check if the resource we are haveing a conversation with is still online
 205					if (conversation.hasValidOtrSession()) {
 206						String otrResource = conversation.getOtrSession().getSessionID().getUserID();
 207						if (!(Arrays.asList(contact.getPresences().toResourceArray()).contains(otrResource))) {
 208							conversation.endOtrIfNeeded();
 209						}
 210					}
 211				}
 212			}
 213		}
 214	};
 215	private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
 216			this);
 217	private AvatarService mAvatarService = new AvatarService(this);
 218	private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
 219	private PushManagementService mPushManagementService = new PushManagementService(this);
 220	private OnConversationUpdate mOnConversationUpdate = null;
 221
 222
 223	private final ConversationsFileObserver fileObserver = new ConversationsFileObserver(
 224			Environment.getExternalStorageDirectory().getAbsolutePath()
 225	) {
 226		@Override
 227		public void onEvent(int event, String path) {
 228			markFileDeleted(path);
 229		}
 230	};
 231	private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
 232
 233		@Override
 234		public void onJinglePacketReceived(Account account, JinglePacket packet) {
 235			mJingleConnectionManager.deliverPacket(account, packet);
 236		}
 237	};
 238	private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
 239
 240		@Override
 241		public void onMessageAcknowledged(Account account, String uuid) {
 242			for (final Conversation conversation : getConversations()) {
 243				if (conversation.getAccount() == account) {
 244					Message message = conversation.findUnsentMessageWithUuid(uuid);
 245					if (message != null) {
 246						markMessage(message, Message.STATUS_SEND);
 247					}
 248				}
 249			}
 250		}
 251	};
 252	private int convChangedListenerCount = 0;
 253	private OnShowErrorToast mOnShowErrorToast = null;
 254	private int showErrorToastListenerCount = 0;
 255	private int unreadCount = -1;
 256	private OnAccountUpdate mOnAccountUpdate = null;
 257	private OnCaptchaRequested mOnCaptchaRequested = null;
 258	private int accountChangedListenerCount = 0;
 259	private int captchaRequestedListenerCount = 0;
 260	private OnRosterUpdate mOnRosterUpdate = null;
 261	private OnUpdateBlocklist mOnUpdateBlocklist = null;
 262	private int updateBlocklistListenerCount = 0;
 263	private int rosterChangedListenerCount = 0;
 264	private OnMucRosterUpdate mOnMucRosterUpdate = null;
 265	private int mucRosterChangedListenerCount = 0;
 266	private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
 267	private int keyStatusUpdatedListenerCount = 0;
 268	private SecureRandom mRandom;
 269	private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20);
 270	private final OnBindListener mOnBindListener = new OnBindListener() {
 271
 272		@Override
 273		public void onBind(final Account account) {
 274			synchronized (mInProgressAvatarFetches) {
 275				for (Iterator<String> iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) {
 276					final String KEY = iterator.next();
 277					if (KEY.startsWith(account.getJid().toBareJid() + "_")) {
 278						iterator.remove();
 279					}
 280				}
 281			}
 282			account.getRoster().clearPresences();
 283			mJingleConnectionManager.cancelInTransmission();
 284			fetchRosterFromServer(account);
 285			fetchBookmarks(account);
 286			sendPresence(account);
 287			if (mPushManagementService.available(account)) {
 288				mPushManagementService.registerPushTokenOnServer(account);
 289			}
 290			connectMultiModeConversations(account);
 291			syncDirtyContacts(account);
 292		}
 293	};
 294	private OnStatusChanged statusListener = new OnStatusChanged() {
 295
 296		@Override
 297		public void onStatusChanged(final Account account) {
 298			XmppConnection connection = account.getXmppConnection();
 299			if (mOnAccountUpdate != null) {
 300				mOnAccountUpdate.onAccountUpdate();
 301			}
 302			if (account.getStatus() == Account.State.ONLINE) {
 303				synchronized (mLowPingTimeoutMode) {
 304					if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) {
 305						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode");
 306					}
 307				}
 308				if (account.setShowErrorNotification(true)) {
 309					databaseBackend.updateAccount(account);
 310				}
 311				mMessageArchiveService.executePendingQueries(account);
 312				if (connection != null && connection.getFeatures().csi()) {
 313					if (checkListeners()) {
 314						Log.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//inactive");
 315						connection.sendInactive();
 316					} else {
 317						Log.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//active");
 318						connection.sendActive();
 319					}
 320				}
 321				List<Conversation> conversations = getConversations();
 322				for (Conversation conversation : conversations) {
 323					if (conversation.getAccount() == account
 324							&& !account.pendingConferenceJoins.contains(conversation)) {
 325						if (!conversation.startOtrIfNeeded()) {
 326							Log.d(Config.LOGTAG,account.getJid().toBareJid()+": couldn't start OTR with "+conversation.getContact().getJid()+" when needed");
 327						}
 328						sendUnsentMessages(conversation);
 329					}
 330				}
 331				for (Conversation conversation : account.pendingConferenceLeaves) {
 332					leaveMuc(conversation);
 333				}
 334				account.pendingConferenceLeaves.clear();
 335				for (Conversation conversation : account.pendingConferenceJoins) {
 336					joinMuc(conversation);
 337				}
 338				account.pendingConferenceJoins.clear();
 339				scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
 340			} else {
 341				if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
 342					resetSendingToWaiting(account);
 343					if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 344						synchronized (mLowPingTimeoutMode) {
 345							if (mLowPingTimeoutMode.contains(account.getJid().toBareJid())) {
 346								Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": went into offline state during low ping mode. reconnecting now");
 347								reconnectAccount(account, true, false);
 348							} else {
 349								int timeToReconnect = mRandom.nextInt(10) + 2;
 350								scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
 351							}
 352						}
 353					}
 354				} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
 355					databaseBackend.updateAccount(account);
 356					reconnectAccount(account, true, false);
 357				} else if ((account.getStatus() != Account.State.CONNECTING)
 358						&& (account.getStatus() != Account.State.NO_INTERNET)) {
 359					resetSendingToWaiting(account);
 360					if (connection != null) {
 361						int next = connection.getTimeToNextAttempt();
 362						Log.d(Config.LOGTAG, account.getJid().toBareJid()
 363								+ ": error connecting account. try again in "
 364								+ next + "s for the "
 365								+ (connection.getAttempt() + 1) + " time");
 366						scheduleWakeUpCall(next, account.getUuid().hashCode());
 367					}
 368				}
 369			}
 370			getNotificationService().updateErrorNotification();
 371		}
 372	};
 373	private OpenPgpServiceConnection pgpServiceConnection;
 374	private PgpEngine mPgpEngine = null;
 375	private WakeLock wakeLock;
 376	private PowerManager pm;
 377	private LruCache<String, Bitmap> mBitmapCache;
 378	private EventReceiver mEventReceiver = new EventReceiver();
 379
 380	private boolean mRestoredFromDatabase = false;
 381
 382	private static String generateFetchKey(Account account, final Avatar avatar) {
 383		return account.getJid().toBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum;
 384	}
 385
 386	public boolean areMessagesInitialized() {
 387		return this.mRestoredFromDatabase;
 388	}
 389
 390	public PgpEngine getPgpEngine() {
 391		if (!Config.supportOpenPgp()) {
 392			return null;
 393		} else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
 394			if (this.mPgpEngine == null) {
 395				this.mPgpEngine = new PgpEngine(new OpenPgpApi(
 396						getApplicationContext(),
 397						pgpServiceConnection.getService()), this);
 398			}
 399			return mPgpEngine;
 400		} else {
 401			return null;
 402		}
 403
 404	}
 405
 406	public OpenPgpApi getOpenPgpApi() {
 407		if (!Config.supportOpenPgp()) {
 408			return null;
 409		} else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
 410			return new OpenPgpApi(this, pgpServiceConnection.getService());
 411		} else {
 412			return null;
 413		}
 414	}
 415
 416	public FileBackend getFileBackend() {
 417		return this.fileBackend;
 418	}
 419
 420	public AvatarService getAvatarService() {
 421		return this.mAvatarService;
 422	}
 423
 424	public void attachLocationToConversation(final Conversation conversation,
 425											 final Uri uri,
 426											 final UiCallback<Message> callback) {
 427		int encryption = conversation.getNextEncryption();
 428		if (encryption == Message.ENCRYPTION_PGP) {
 429			encryption = Message.ENCRYPTION_DECRYPTED;
 430		}
 431		Message message = new Message(conversation, uri.toString(), encryption);
 432		if (conversation.getNextCounterpart() != null) {
 433			message.setCounterpart(conversation.getNextCounterpart());
 434		}
 435		if (encryption == Message.ENCRYPTION_DECRYPTED) {
 436			getPgpEngine().encrypt(message, callback);
 437		} else {
 438			callback.success(message);
 439		}
 440	}
 441
 442	public void attachFileToConversation(final Conversation conversation,
 443										 final Uri uri,
 444										 final UiCallback<Message> callback) {
 445		if (FileBackend.weOwnFile(this, uri)) {
 446			Log.d(Config.LOGTAG,"trying to attach file that belonged to us");
 447			callback.error(R.string.security_error_invalid_file_access, null);
 448			return;
 449		}
 450		final Message message;
 451		if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 452			message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
 453		} else {
 454			message = new Message(conversation, "", conversation.getNextEncryption());
 455		}
 456		message.setCounterpart(conversation.getNextCounterpart());
 457		message.setType(Message.TYPE_FILE);
 458		final String path = getFileBackend().getOriginalPath(uri);
 459		mFileAddingExecutor.execute(new Runnable() {
 460			@Override
 461			public void run() {
 462				if (path != null) {
 463					message.setRelativeFilePath(path);
 464					getFileBackend().updateFileParams(message);
 465					if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 466						getPgpEngine().encrypt(message, callback);
 467					} else {
 468						callback.success(message);
 469					}
 470				} else {
 471					try {
 472						getFileBackend().copyFileToPrivateStorage(message, uri);
 473						getFileBackend().updateFileParams(message);
 474						if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 475							final PgpEngine pgpEngine = getPgpEngine();
 476							if (pgpEngine != null) {
 477								pgpEngine.encrypt(message, callback);
 478							} else if (callback != null) {
 479								callback.error(R.string.unable_to_connect_to_keychain, null);
 480							}
 481						} else {
 482							callback.success(message);
 483						}
 484					} catch (FileBackend.FileCopyException e) {
 485						callback.error(e.getResId(), message);
 486					}
 487				}
 488			}
 489		});
 490	}
 491
 492	public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
 493		if (FileBackend.weOwnFile(this, uri)) {
 494			Log.d(Config.LOGTAG,"trying to attach file that belonged to us");
 495			callback.error(R.string.security_error_invalid_file_access, null);
 496			return;
 497		}
 498
 499		final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
 500		final String compressPictures = getCompressPicturesPreference();
 501
 502		if ("never".equals(compressPictures)
 503				|| ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))
 504				|| (mimeType != null && mimeType.endsWith("/gif"))) {
 505			Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+ ": not compressing picture. sending as file");
 506			attachFileToConversation(conversation, uri, callback);
 507			return;
 508		}
 509		final Message message;
 510		if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 511			message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
 512		} else {
 513			message = new Message(conversation, "", conversation.getNextEncryption());
 514		}
 515		message.setCounterpart(conversation.getNextCounterpart());
 516		message.setType(Message.TYPE_IMAGE);
 517		mFileAddingExecutor.execute(new Runnable() {
 518
 519			@Override
 520			public void run() {
 521				try {
 522					getFileBackend().copyImageToPrivateStorage(message, uri);
 523					if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 524						final PgpEngine pgpEngine = getPgpEngine();
 525						if (pgpEngine != null) {
 526							pgpEngine.encrypt(message, callback);
 527						} else if (callback != null){
 528							callback.error(R.string.unable_to_connect_to_keychain, null);
 529						}
 530					} else {
 531						callback.success(message);
 532					}
 533				} catch (final FileBackend.FileCopyException e) {
 534					callback.error(e.getResId(), message);
 535				}
 536			}
 537		});
 538	}
 539
 540	public Conversation find(Bookmark bookmark) {
 541		return find(bookmark.getAccount(), bookmark.getJid());
 542	}
 543
 544	public Conversation find(final Account account, final Jid jid) {
 545		return find(getConversations(), account, jid);
 546	}
 547
 548	@Override
 549	public int onStartCommand(Intent intent, int flags, int startId) {
 550		final String action = intent == null ? null : intent.getAction();
 551		String pushedAccountHash = null;
 552		boolean interactive = false;
 553		if (action != null) {
 554			final Conversation c = findConversationByUuid(intent.getStringExtra("uuid"));
 555			switch (action) {
 556				case ConnectivityManager.CONNECTIVITY_ACTION:
 557					if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
 558						resetAllAttemptCounts(true, false);
 559					}
 560					break;
 561				case ACTION_MERGE_PHONE_CONTACTS:
 562					if (mRestoredFromDatabase) {
 563						loadPhoneContacts();
 564					}
 565					return START_STICKY;
 566				case Intent.ACTION_SHUTDOWN:
 567					logoutAndSave(true);
 568					return START_NOT_STICKY;
 569				case ACTION_CLEAR_NOTIFICATION:
 570					if (c != null) {
 571						mNotificationService.clear(c);
 572					} else {
 573						mNotificationService.clear();
 574					}
 575					break;
 576				case ACTION_DISABLE_FOREGROUND:
 577					getPreferences().edit().putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, false).commit();
 578					toggleForegroundService();
 579					break;
 580				case ACTION_DISMISS_ERROR_NOTIFICATIONS:
 581					dismissErrorNotifications();
 582					break;
 583				case ACTION_TRY_AGAIN:
 584					resetAllAttemptCounts(false, true);
 585					interactive = true;
 586					break;
 587				case ACTION_REPLY_TO_CONVERSATION:
 588					Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
 589					if (remoteInput != null && c != null) {
 590						final CharSequence body = remoteInput.getCharSequence("text_reply");
 591						if (body != null && body.length() > 0) {
 592							directReply(c, body.toString(),intent.getBooleanExtra("dismiss_notification",false));
 593						}
 594					}
 595					break;
 596				case AudioManager.RINGER_MODE_CHANGED_ACTION:
 597					if (xaOnSilentMode()) {
 598						refreshAllPresences();
 599					}
 600					break;
 601				case Intent.ACTION_SCREEN_ON:
 602					deactivateGracePeriod();
 603				case Intent.ACTION_SCREEN_OFF:
 604					if (awayWhenScreenOff()) {
 605						refreshAllPresences();
 606					}
 607					break;
 608				case ACTION_GCM_TOKEN_REFRESH:
 609					refreshAllGcmTokens();
 610					break;
 611				case ACTION_IDLE_PING:
 612					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 613						scheduleNextIdlePing();
 614					}
 615					break;
 616				case ACTION_GCM_MESSAGE_RECEIVED:
 617					Log.d(Config.LOGTAG,"gcm push message arrived in service. extras="+intent.getExtras());
 618					pushedAccountHash = intent.getStringExtra("account");
 619					break;
 620			}
 621		}
 622		synchronized (this) {
 623			this.wakeLock.acquire();
 624			boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action);
 625			HashSet<Account> pingCandidates = new HashSet<>();
 626			for (Account account : accounts) {
 627				pingNow |= processAccountState(account,
 628						interactive,
 629						"ui".equals(action),
 630						CryptoHelper.getAccountFingerprint(account).equals(pushedAccountHash),
 631						pingCandidates);
 632			}
 633			if (pingNow) {
 634				for (Account account : pingCandidates) {
 635					final boolean lowTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid());
 636					account.getXmppConnection().sendPing();
 637					Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping (action=" + action + ",lowTimeout=" + Boolean.toString(lowTimeout) + ")");
 638					scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode());
 639				}
 640			}
 641			if (wakeLock.isHeld()) {
 642				try {
 643					wakeLock.release();
 644				} catch (final RuntimeException ignored) {
 645				}
 646			}
 647		}
 648		return START_STICKY;
 649	}
 650
 651	private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet<Account> pingCandidates) {
 652		boolean pingNow = false;
 653		if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 654			if (!hasInternetConnection()) {
 655				account.setStatus(Account.State.NO_INTERNET);
 656				if (statusListener != null) {
 657					statusListener.onStatusChanged(account);
 658				}
 659			} else {
 660				if (account.getStatus() == Account.State.NO_INTERNET) {
 661					account.setStatus(Account.State.OFFLINE);
 662					if (statusListener != null) {
 663						statusListener.onStatusChanged(account);
 664					}
 665				}
 666				if (account.getStatus() == Account.State.ONLINE) {
 667					synchronized (mLowPingTimeoutMode) {
 668						long lastReceived = account.getXmppConnection().getLastPacketReceived();
 669						long lastSent = account.getXmppConnection().getLastPingSent();
 670						long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
 671						long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
 672						int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000;
 673						long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
 674						if (lastSent > lastReceived) {
 675							if (pingTimeoutIn < 0) {
 676								Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout");
 677								this.reconnectAccount(account, true, interactive);
 678							} else {
 679								int secs = (int) (pingTimeoutIn / 1000);
 680								this.scheduleWakeUpCall(secs, account.getUuid().hashCode());
 681							}
 682						} else {
 683							pingCandidates.add(account);
 684							if (isAccountPushed) {
 685								pingNow = true;
 686								if (mLowPingTimeoutMode.add(account.getJid().toBareJid())) {
 687									Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": entering low ping timeout mode");
 688								}
 689							} else if (msToNextPing <= 0) {
 690								pingNow = true;
 691							} else {
 692								this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
 693								if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) {
 694									Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode");
 695								}
 696							}
 697						}
 698					}
 699				} else if (account.getStatus() == Account.State.OFFLINE) {
 700					reconnectAccount(account, true, interactive);
 701				} else if (account.getStatus() == Account.State.CONNECTING) {
 702					long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
 703					long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
 704					long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
 705					long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
 706					if (timeout < 0) {
 707						Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast="+secondsSinceLastConnect+")");
 708						account.getXmppConnection().resetAttemptCount(false);
 709						reconnectAccount(account, true, interactive);
 710					} else if (discoTimeout < 0) {
 711						account.getXmppConnection().sendDiscoTimeout();
 712						scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
 713					} else {
 714						scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
 715					}
 716				} else {
 717					if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
 718						reconnectAccount(account, true, interactive);
 719					}
 720				}
 721			}
 722		}
 723		return pingNow;
 724	}
 725
 726	public boolean isDataSaverDisabled() {
 727		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 728			ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
 729			return !connectivityManager.isActiveNetworkMetered()
 730					|| connectivityManager.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
 731		} else {
 732			return true;
 733		}
 734	}
 735
 736	private void directReply(Conversation conversation, String body, final boolean dismissAfterReply) {
 737		Message message = new Message(conversation,body,conversation.getNextEncryption());
 738		message.markUnread();
 739		if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 740			getPgpEngine().encrypt(message, new UiCallback<Message>() {
 741				@Override
 742				public void success(Message message) {
 743					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 744					sendMessage(message);
 745					if (dismissAfterReply) {
 746						markRead(message.getConversation(),true);
 747					} else {
 748						mNotificationService.pushFromDirectReply(message);
 749					}
 750				}
 751
 752				@Override
 753				public void error(int errorCode, Message object) {
 754
 755				}
 756
 757				@Override
 758				public void userInputRequried(PendingIntent pi, Message object) {
 759
 760				}
 761			});
 762		} else {
 763			sendMessage(message);
 764			if (dismissAfterReply) {
 765				markRead(conversation,true);
 766			} else {
 767				mNotificationService.pushFromDirectReply(message);
 768			}
 769		}
 770	}
 771
 772	private boolean xaOnSilentMode() {
 773		return getPreferences().getBoolean("xa_on_silent_mode", false);
 774	}
 775
 776	private boolean manuallyChangePresence() {
 777		return getPreferences().getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, false);
 778	}
 779
 780	private boolean treatVibrateAsSilent() {
 781		return getPreferences().getBoolean(SettingsActivity.TREAT_VIBRATE_AS_SILENT, false);
 782	}
 783
 784	private boolean awayWhenScreenOff() {
 785		return getPreferences().getBoolean(SettingsActivity.AWAY_WHEN_SCREEN_IS_OFF, false);
 786	}
 787
 788	private String getCompressPicturesPreference() {
 789		return getPreferences().getString("picture_compression", "auto");
 790	}
 791
 792	private Presence.Status getTargetPresence() {
 793		if (xaOnSilentMode() && isPhoneSilenced()) {
 794			return Presence.Status.XA;
 795		} else if (awayWhenScreenOff() && !isInteractive()) {
 796			return Presence.Status.AWAY;
 797		} else {
 798			return Presence.Status.ONLINE;
 799		}
 800	}
 801
 802	@SuppressLint("NewApi")
 803	@SuppressWarnings("deprecation")
 804	public boolean isInteractive() {
 805		final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 806
 807		final boolean isScreenOn;
 808		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
 809			isScreenOn = pm.isScreenOn();
 810		} else {
 811			isScreenOn = pm.isInteractive();
 812		}
 813		return isScreenOn;
 814	}
 815
 816	private boolean isPhoneSilenced() {
 817		AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
 818		try {
 819			if (treatVibrateAsSilent()) {
 820				return audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
 821			} else {
 822				return audioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT;
 823			}
 824		} catch (Throwable throwable) {
 825			Log.d(Config.LOGTAG,"platform bug in isPhoneSilenced ("+ throwable.getMessage()+")");
 826			return false;
 827		}
 828	}
 829
 830	private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
 831		Log.d(Config.LOGTAG, "resetting all attempt counts");
 832		for (Account account : accounts) {
 833			if (account.hasErrorStatus() || reallyAll) {
 834				final XmppConnection connection = account.getXmppConnection();
 835				if (connection != null) {
 836					connection.resetAttemptCount(retryImmediately);
 837				}
 838			}
 839			if (account.setShowErrorNotification(true)) {
 840				databaseBackend.updateAccount(account);
 841			}
 842		}
 843		mNotificationService.updateErrorNotification();
 844	}
 845
 846	private void dismissErrorNotifications() {
 847		for (final Account account : this.accounts) {
 848			if (account.hasErrorStatus()) {
 849				Log.d(Config.LOGTAG,account.getJid().toBareJid()+": dismissing error notification");
 850				if (account.setShowErrorNotification(false)) {
 851					databaseBackend.updateAccount(account);
 852				}
 853			}
 854		}
 855	}
 856
 857	public boolean hasInternetConnection() {
 858		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
 859				.getSystemService(Context.CONNECTIVITY_SERVICE);
 860		NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
 861		return activeNetwork != null && activeNetwork.isConnected();
 862	}
 863
 864	@SuppressLint("TrulyRandom")
 865	@Override
 866	public void onCreate() {
 867		ExceptionHelper.init(getApplicationContext());
 868		PRNGFixes.apply();
 869		this.mRandom = new SecureRandom();
 870		updateMemorizingTrustmanager();
 871		final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
 872		final int cacheSize = maxMemory / 8;
 873		this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
 874			@Override
 875			protected int sizeOf(final String key, final Bitmap bitmap) {
 876				return bitmap.getByteCount() / 1024;
 877			}
 878		};
 879
 880		this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
 881		this.accounts = databaseBackend.getAccounts();
 882
 883		if (Config.FREQUENT_RESTARTS_THRESHOLD != 0
 884				&& Config.FREQUENT_RESTARTS_DETECTION_WINDOW != 0
 885				&& !keepForegroundService()
 886				&& databaseBackend.startTimeCountExceedsThreshold()) {
 887			getPreferences().edit().putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE,true).commit();
 888			Log.d(Config.LOGTAG,"number of restarts exceeds threshold. enabling foreground service");
 889		}
 890
 891		restoreFromDatabase();
 892
 893		getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
 894		new Thread(new Runnable() {
 895			@Override
 896			public void run() {
 897				fileObserver.startWatching();
 898			}
 899		}).start();
 900		if (Config.supportOpenPgp()) {
 901			this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
 902				@Override
 903				public void onBound(IOpenPgpService2 service) {
 904					for (Account account : accounts) {
 905						final PgpDecryptionService pgp = account.getPgpDecryptionService();
 906						if(pgp != null) {
 907							pgp.continueDecryption(true);
 908						}
 909					}
 910				}
 911
 912				@Override
 913				public void onError(Exception e) {
 914				}
 915			});
 916			this.pgpServiceConnection.bindToService();
 917		}
 918
 919		this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 920		this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService");
 921
 922		toggleForegroundService();
 923		updateUnreadCountBadge();
 924		toggleScreenEventReceiver();
 925		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 926			scheduleNextIdlePing();
 927		}
 928		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 929			registerReceiver(this.mEventReceiver,new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
 930		}
 931	}
 932
 933	@Override
 934	public void onTrimMemory(int level) {
 935		super.onTrimMemory(level);
 936		if (level >= TRIM_MEMORY_COMPLETE) {
 937			Log.d(Config.LOGTAG, "clear cache due to low memory");
 938			getBitmapCache().evictAll();
 939		}
 940	}
 941
 942	@Override
 943	public void onDestroy() {
 944		try {
 945			unregisterReceiver(this.mEventReceiver);
 946		} catch (IllegalArgumentException e) {
 947			//ignored
 948		}
 949		fileObserver.stopWatching();
 950		super.onDestroy();
 951	}
 952
 953	public void toggleScreenEventReceiver() {
 954		if (awayWhenScreenOff() && !manuallyChangePresence()) {
 955			final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
 956			filter.addAction(Intent.ACTION_SCREEN_OFF);
 957			registerReceiver(this.mEventReceiver, filter);
 958		} else {
 959			try {
 960				unregisterReceiver(this.mEventReceiver);
 961			} catch (IllegalArgumentException e) {
 962				//ignored
 963			}
 964		}
 965	}
 966
 967	public void toggleForegroundService() {
 968		if (keepForegroundService()) {
 969			startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
 970		} else {
 971			stopForeground(true);
 972		}
 973	}
 974
 975	private boolean keepForegroundService() {
 976		return getPreferences().getBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE,false);
 977	}
 978
 979	@Override
 980	public void onTaskRemoved(final Intent rootIntent) {
 981		super.onTaskRemoved(rootIntent);
 982		if (!keepForegroundService()) {
 983			this.logoutAndSave(false);
 984		} else {
 985			Log.d(Config.LOGTAG,"ignoring onTaskRemoved because foreground service is activated");
 986		}
 987	}
 988
 989	private void logoutAndSave(boolean stop) {
 990		int activeAccounts = 0;
 991		databaseBackend.clearStartTimeCounter(true); // regular swipes don't count towards restart counter
 992		for (final Account account : accounts) {
 993			if (account.getStatus() != Account.State.DISABLED) {
 994				activeAccounts++;
 995			}
 996			databaseBackend.writeRoster(account.getRoster());
 997			if (account.getXmppConnection() != null) {
 998				new Thread(new Runnable() {
 999					@Override
1000					public void run() {
1001						disconnect(account, false);
1002					}
1003				}).start();
1004			}
1005		}
1006		if (stop || activeAccounts == 0) {
1007			Log.d(Config.LOGTAG, "good bye");
1008			stopSelf();
1009		}
1010	}
1011
1012	public void scheduleWakeUpCall(int seconds, int requestCode) {
1013		final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000;
1014		AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
1015		Intent intent = new Intent(this, EventReceiver.class);
1016		intent.setAction("ping");
1017		PendingIntent alarmIntent = PendingIntent.getBroadcast(this, requestCode, intent, 0);
1018		alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, alarmIntent);
1019	}
1020
1021	@TargetApi(Build.VERSION_CODES.M)
1022	private void scheduleNextIdlePing() {
1023		Log.d(Config.LOGTAG,"schedule next idle ping");
1024		AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
1025		Intent intent = new Intent(this, EventReceiver.class);
1026		intent.setAction(ACTION_IDLE_PING);
1027		alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
1028				SystemClock.elapsedRealtime()+(Config.IDLE_PING_INTERVAL * 1000),
1029				PendingIntent.getBroadcast(this,0,intent,0)
1030				);
1031	}
1032
1033	public XmppConnection createConnection(final Account account) {
1034		final SharedPreferences sharedPref = getPreferences();
1035		String resource;
1036		try {
1037			resource = sharedPref.getString("resource", getString(R.string.default_resource)).toLowerCase(Locale.ENGLISH);
1038			if (resource.trim().isEmpty()) {
1039				throw new Exception();
1040			}
1041		} catch (Exception e) {
1042			resource = "conversations";
1043		}
1044		account.setResource(resource);
1045		final XmppConnection connection = new XmppConnection(account, this);
1046		connection.setOnMessagePacketReceivedListener(this.mMessageParser);
1047		connection.setOnStatusChangedListener(this.statusListener);
1048		connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
1049		connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
1050		connection.setOnJinglePacketReceivedListener(this.jingleListener);
1051		connection.setOnBindListener(this.mOnBindListener);
1052		connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
1053		connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
1054		connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService);
1055		AxolotlService axolotlService = account.getAxolotlService();
1056		if (axolotlService != null) {
1057			connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
1058		}
1059		return connection;
1060	}
1061
1062	public void sendChatState(Conversation conversation) {
1063		if (sendChatStates()) {
1064			MessagePacket packet = mMessageGenerator.generateChatState(conversation);
1065			sendMessagePacket(conversation.getAccount(), packet);
1066		}
1067	}
1068
1069	private void sendFileMessage(final Message message, final boolean delay) {
1070		Log.d(Config.LOGTAG, "send file message");
1071		final Account account = message.getConversation().getAccount();
1072		if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())) {
1073			mHttpConnectionManager.createNewUploadConnection(message, delay);
1074		} else {
1075			mJingleConnectionManager.createNewConnection(message);
1076		}
1077	}
1078
1079	public void sendMessage(final Message message) {
1080		sendMessage(message, false, false);
1081	}
1082
1083	private void sendMessage(final Message message, final boolean resend, final boolean delay) {
1084		final Account account = message.getConversation().getAccount();
1085		if (account.setShowErrorNotification(true)) {
1086			databaseBackend.updateAccount(account);
1087			mNotificationService.updateErrorNotification();
1088		}
1089		final Conversation conversation = message.getConversation();
1090		account.deactivateGracePeriod();
1091		MessagePacket packet = null;
1092		final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI
1093				|| account.getServerIdentity() != XmppConnection.Identity.SLACK)
1094				&& !message.edited();
1095		boolean saveInDb = addToConversation;
1096		message.setStatus(Message.STATUS_WAITING);
1097
1098		if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
1099			message.getConversation().endOtrIfNeeded();
1100			message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
1101					new Conversation.OnMessageFound() {
1102						@Override
1103						public void onMessageFound(Message message) {
1104							markMessage(message, Message.STATUS_SEND_FAILED);
1105						}
1106					});
1107		}
1108
1109		if (account.isOnlineAndConnected()) {
1110			switch (message.getEncryption()) {
1111				case Message.ENCRYPTION_NONE:
1112					if (message.needsUploading()) {
1113						if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())
1114								|| message.fixCounterpart()) {
1115							this.sendFileMessage(message, delay);
1116						} else {
1117							break;
1118						}
1119					} else {
1120						packet = mMessageGenerator.generateChat(message);
1121					}
1122					break;
1123				case Message.ENCRYPTION_PGP:
1124				case Message.ENCRYPTION_DECRYPTED:
1125					if (message.needsUploading()) {
1126						if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())
1127								|| message.fixCounterpart()) {
1128							this.sendFileMessage(message, delay);
1129						} else {
1130							break;
1131						}
1132					} else {
1133						packet = mMessageGenerator.generatePgpChat(message);
1134					}
1135					break;
1136				case Message.ENCRYPTION_OTR:
1137					SessionImpl otrSession = conversation.getOtrSession();
1138					if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
1139						try {
1140							message.setCounterpart(Jid.fromSessionID(otrSession.getSessionID()));
1141						} catch (InvalidJidException e) {
1142							break;
1143						}
1144						if (message.needsUploading()) {
1145							mJingleConnectionManager.createNewConnection(message);
1146						} else {
1147							packet = mMessageGenerator.generateOtrChat(message);
1148						}
1149					} else if (otrSession == null) {
1150						if (message.fixCounterpart()) {
1151							conversation.startOtrSession(message.getCounterpart().getResourcepart(), true);
1152						} else {
1153							Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not fix counterpart for OTR message to contact "+message.getContact().getJid());
1154							break;
1155						}
1156					} else {
1157						Log.d(Config.LOGTAG,account.getJid().toBareJid()+" OTR session with "+message.getContact()+" is in wrong state: "+otrSession.getSessionStatus().toString());
1158					}
1159					break;
1160				case Message.ENCRYPTION_AXOLOTL:
1161					message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
1162					if (message.needsUploading()) {
1163						if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())
1164								|| message.fixCounterpart()) {
1165							this.sendFileMessage(message, delay);
1166						} else {
1167							break;
1168						}
1169					} else {
1170						XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
1171						if (axolotlMessage == null) {
1172							account.getAxolotlService().preparePayloadMessage(message, delay);
1173						} else {
1174							packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
1175						}
1176					}
1177					break;
1178
1179			}
1180			if (packet != null) {
1181				if (account.getXmppConnection().getFeatures().sm()
1182						|| (conversation.getMode() == Conversation.MODE_MULTI && message.getCounterpart().isBareJid())) {
1183					message.setStatus(Message.STATUS_UNSEND);
1184				} else {
1185					message.setStatus(Message.STATUS_SEND);
1186				}
1187			}
1188		} else {
1189			switch (message.getEncryption()) {
1190				case Message.ENCRYPTION_DECRYPTED:
1191					if (!message.needsUploading()) {
1192						String pgpBody = message.getEncryptedBody();
1193						String decryptedBody = message.getBody();
1194						message.setBody(pgpBody);
1195						message.setEncryption(Message.ENCRYPTION_PGP);
1196						if (message.edited()) {
1197							message.setBody(decryptedBody);
1198							message.setEncryption(Message.ENCRYPTION_DECRYPTED);
1199							databaseBackend.updateMessage(message, message.getEditedId());
1200							updateConversationUi();
1201							return;
1202						} else {
1203							databaseBackend.createMessage(message);
1204							saveInDb = false;
1205							message.setBody(decryptedBody);
1206							message.setEncryption(Message.ENCRYPTION_DECRYPTED);
1207						}
1208					}
1209					break;
1210				case Message.ENCRYPTION_OTR:
1211					if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) {
1212						Log.d(Config.LOGTAG,account.getJid().toBareJid()+": create otr session without starting for "+message.getContact().getJid());
1213						conversation.startOtrSession(message.getCounterpart().getResourcepart(), false);
1214					}
1215					break;
1216				case Message.ENCRYPTION_AXOLOTL:
1217					message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
1218					break;
1219			}
1220		}
1221
1222		if (resend) {
1223			if (packet != null && addToConversation) {
1224				if (account.getXmppConnection().getFeatures().sm()
1225						|| (conversation.getMode() == Conversation.MODE_MULTI && message.getCounterpart().isBareJid())) {
1226					markMessage(message, Message.STATUS_UNSEND);
1227				} else {
1228					markMessage(message, Message.STATUS_SEND);
1229				}
1230			}
1231		} else {
1232			if (addToConversation) {
1233				conversation.add(message);
1234			}
1235			if (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages()) {
1236				if (saveInDb) {
1237					databaseBackend.createMessage(message);
1238				} else if (message.edited()) {
1239					databaseBackend.updateMessage(message, message.getEditedId());
1240				}
1241			}
1242			updateConversationUi();
1243		}
1244		if (packet != null) {
1245			if (delay) {
1246				mMessageGenerator.addDelay(packet, message.getTimeSent());
1247			}
1248			if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
1249				if (this.sendChatStates()) {
1250					packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
1251				}
1252			}
1253			sendMessagePacket(account, packet);
1254		}
1255	}
1256
1257	private void sendUnsentMessages(final Conversation conversation) {
1258		conversation.findWaitingMessages(new Conversation.OnMessageFound() {
1259
1260			@Override
1261			public void onMessageFound(Message message) {
1262				resendMessage(message, true);
1263			}
1264		});
1265	}
1266
1267	public void resendMessage(final Message message, final boolean delay) {
1268		sendMessage(message, true, delay);
1269	}
1270
1271	public void fetchRosterFromServer(final Account account) {
1272		final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
1273		if (!"".equals(account.getRosterVersion())) {
1274			Log.d(Config.LOGTAG, account.getJid().toBareJid()
1275					+ ": fetching roster version " + account.getRosterVersion());
1276		} else {
1277			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
1278		}
1279		iqPacket.query(Xmlns.ROSTER).setAttribute("ver", account.getRosterVersion());
1280		sendIqPacket(account, iqPacket, mIqParser);
1281	}
1282
1283	public void fetchBookmarks(final Account account) {
1284		final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
1285		final Element query = iqPacket.query("jabber:iq:private");
1286		query.addChild("storage", "storage:bookmarks");
1287		final OnIqPacketReceived callback = new OnIqPacketReceived() {
1288
1289			@Override
1290			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1291				if (packet.getType() == IqPacket.TYPE.RESULT) {
1292					final Element query = packet.query();
1293					final HashMap<Jid, Bookmark> bookmarks = new HashMap<>();
1294					final Element storage = query.findChild("storage", "storage:bookmarks");
1295					final boolean autojoin = respectAutojoin();
1296					if (storage != null) {
1297						for (final Element item : storage.getChildren()) {
1298							if (item.getName().equals("conference")) {
1299								final Bookmark bookmark = Bookmark.parse(item, account);
1300								Bookmark old = bookmarks.put(bookmark.getJid(), bookmark);
1301								if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) {
1302									bookmark.setBookmarkName(old.getBookmarkName());
1303								}
1304								Conversation conversation = find(bookmark);
1305								if (conversation != null) {
1306									conversation.setBookmark(bookmark);
1307								} else if (bookmark.autojoin() && bookmark.getJid() != null && autojoin) {
1308									conversation = findOrCreateConversation(
1309											account, bookmark.getJid(), true);
1310									conversation.setBookmark(bookmark);
1311									joinMuc(conversation);
1312								}
1313							}
1314						}
1315					}
1316					account.setBookmarks(new ArrayList<>(bookmarks.values()));
1317				} else {
1318					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not fetch bookmarks");
1319				}
1320			}
1321		};
1322		sendIqPacket(account, iqPacket, callback);
1323	}
1324
1325	public void pushBookmarks(Account account) {
1326		Log.d(Config.LOGTAG, account.getJid().toBareJid()+": pushing bookmarks");
1327		IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
1328		Element query = iqPacket.query("jabber:iq:private");
1329		Element storage = query.addChild("storage", "storage:bookmarks");
1330		for (Bookmark bookmark : account.getBookmarks()) {
1331			storage.addChild(bookmark);
1332		}
1333		sendIqPacket(account, iqPacket, mDefaultIqHandler);
1334	}
1335
1336	private void restoreFromDatabase() {
1337		synchronized (this.conversations) {
1338			final Map<String, Account> accountLookupTable = new Hashtable<>();
1339			for (Account account : this.accounts) {
1340				accountLookupTable.put(account.getUuid(), account);
1341			}
1342			this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
1343			for (Conversation conversation : this.conversations) {
1344				Account account = accountLookupTable.get(conversation.getAccountUuid());
1345				conversation.setAccount(account);
1346			}
1347			Runnable runnable = new Runnable() {
1348				@Override
1349				public void run() {
1350					Log.d(Config.LOGTAG, "restoring roster");
1351					for (Account account : accounts) {
1352						databaseBackend.readRoster(account.getRoster());
1353						account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
1354					}
1355					getBitmapCache().evictAll();
1356					loadPhoneContacts();
1357					Log.d(Config.LOGTAG, "restoring messages");
1358					for (Conversation conversation : conversations) {
1359						conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1360						checkDeletedFiles(conversation);
1361						conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
1362
1363							@Override
1364							public void onMessageFound(Message message) {
1365								markMessage(message, Message.STATUS_WAITING);
1366							}
1367						});
1368						conversation.findUnreadMessages(new Conversation.OnMessageFound() {
1369							@Override
1370							public void onMessageFound(Message message) {
1371								mNotificationService.pushFromBacklog(message);
1372							}
1373						});
1374					}
1375					mNotificationService.finishBacklog(false);
1376					mRestoredFromDatabase = true;
1377					Log.d(Config.LOGTAG, "restored all messages");
1378					updateConversationUi();
1379				}
1380			};
1381			mDatabaseExecutor.execute(runnable);
1382		}
1383	}
1384
1385	public void loadPhoneContacts() {
1386		mContactMergerExecutor.execute(new Runnable() {
1387			@Override
1388			public void run() {
1389				PhoneHelper.loadPhoneContacts(XmppConnectionService.this, new OnPhoneContactsLoadedListener() {
1390					@Override
1391					public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
1392						Log.d(Config.LOGTAG, "start merging phone contacts with roster");
1393						for (Account account : accounts) {
1394							List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts();
1395							for (Bundle phoneContact : phoneContacts) {
1396								Jid jid;
1397								try {
1398									jid = Jid.fromString(phoneContact.getString("jid"));
1399								} catch (final InvalidJidException e) {
1400									continue;
1401								}
1402								final Contact contact = account.getRoster().getContact(jid);
1403								String systemAccount = phoneContact.getInt("phoneid")
1404										+ "#"
1405										+ phoneContact.getString("lookup");
1406								contact.setSystemAccount(systemAccount);
1407								if (contact.setPhotoUri(phoneContact.getString("photouri"))) {
1408									getAvatarService().clear(contact);
1409								}
1410								contact.setSystemName(phoneContact.getString("displayname"));
1411								withSystemAccounts.remove(contact);
1412							}
1413							for (Contact contact : withSystemAccounts) {
1414								contact.setSystemAccount(null);
1415								contact.setSystemName(null);
1416								if (contact.setPhotoUri(null)) {
1417									getAvatarService().clear(contact);
1418								}
1419							}
1420						}
1421						Log.d(Config.LOGTAG, "finished merging phone contacts");
1422						updateAccountUi();
1423					}
1424				});
1425			}
1426		});
1427	}
1428
1429	public List<Conversation> getConversations() {
1430		return this.conversations;
1431	}
1432
1433	private void checkDeletedFiles(Conversation conversation) {
1434		conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
1435
1436			@Override
1437			public void onMessageFound(Message message) {
1438				if (!getFileBackend().isFileAvailable(message)) {
1439					message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1440					final int s = message.getStatus();
1441					if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
1442						markMessage(message, Message.STATUS_SEND_FAILED);
1443					}
1444				}
1445			}
1446		});
1447	}
1448
1449	private void markFileDeleted(final String path) {
1450		Log.d(Config.LOGTAG,"deleted file "+path);
1451		for (Conversation conversation : getConversations()) {
1452			conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
1453				@Override
1454				public void onMessageFound(Message message) {
1455					DownloadableFile file = fileBackend.getFile(message);
1456					if (file.getAbsolutePath().equals(path)) {
1457						if (!file.exists()) {
1458							message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1459							final int s = message.getStatus();
1460							if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
1461								markMessage(message, Message.STATUS_SEND_FAILED);
1462							} else {
1463								updateConversationUi();
1464							}
1465						} else {
1466							Log.d(Config.LOGTAG,"found matching message for file "+path+" but file still exists");
1467						}
1468					}
1469				}
1470			});
1471		}
1472	}
1473
1474	public void populateWithOrderedConversations(final List<Conversation> list) {
1475		populateWithOrderedConversations(list, true);
1476	}
1477
1478	public void populateWithOrderedConversations(final List<Conversation> list, boolean includeNoFileUpload) {
1479		list.clear();
1480		if (includeNoFileUpload) {
1481			list.addAll(getConversations());
1482		} else {
1483			for (Conversation conversation : getConversations()) {
1484				if (conversation.getMode() == Conversation.MODE_SINGLE
1485						|| conversation.getAccount().httpUploadAvailable()) {
1486					list.add(conversation);
1487				}
1488			}
1489		}
1490		try {
1491			Collections.sort(list);
1492		} catch (IllegalArgumentException e) {
1493			//ignore
1494		}
1495	}
1496
1497	public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
1498		if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) {
1499			return;
1500		} else if (timestamp == 0) {
1501			return;
1502		}
1503		Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp));
1504		Runnable runnable = new Runnable() {
1505			@Override
1506			public void run() {
1507				final Account account = conversation.getAccount();
1508				List<Message> messages = databaseBackend.getMessages(conversation, 50, timestamp);
1509				if (messages.size() > 0) {
1510					conversation.addAll(0, messages);
1511					checkDeletedFiles(conversation);
1512					callback.onMoreMessagesLoaded(messages.size(), conversation);
1513				} else if (conversation.hasMessagesLeftOnServer()
1514						&& account.isOnlineAndConnected()
1515						&& conversation.getLastClearHistory() == 0) {
1516					if ((conversation.getMode() == Conversation.MODE_SINGLE && account.getXmppConnection().getFeatures().mam())
1517							|| (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().mamSupport())) {
1518						MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp);
1519						if (query != null) {
1520							query.setCallback(callback);
1521						}
1522						callback.informUser(R.string.fetching_history_from_server);
1523					}
1524				}
1525			}
1526		};
1527		mDatabaseExecutor.execute(runnable);
1528	}
1529
1530	public List<Account> getAccounts() {
1531		return this.accounts;
1532	}
1533
1534	public List<Conversation> findAllConferencesWith(Contact contact) {
1535		ArrayList<Conversation> results = new ArrayList<>();
1536		for(Conversation conversation : conversations) {
1537			if (conversation.getMode() == Conversation.MODE_MULTI
1538					&& conversation.getMucOptions().isContactInRoom(contact)) {
1539				results.add(conversation);
1540			}
1541		}
1542		return results;
1543	}
1544
1545	public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
1546		for (final Conversation conversation : haystack) {
1547			if (conversation.getContact() == contact) {
1548				return conversation;
1549			}
1550		}
1551		return null;
1552	}
1553
1554	public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
1555		if (jid == null) {
1556			return null;
1557		}
1558		for (final Conversation conversation : haystack) {
1559			if ((account == null || conversation.getAccount() == account)
1560					&& (conversation.getJid().toBareJid().equals(jid.toBareJid()))) {
1561				return conversation;
1562			}
1563		}
1564		return null;
1565	}
1566
1567	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) {
1568		return this.findOrCreateConversation(account, jid, muc, null);
1569	}
1570
1571	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) {
1572		synchronized (this.conversations) {
1573			Conversation conversation = find(account, jid);
1574			if (conversation != null) {
1575				return conversation;
1576			}
1577			conversation = databaseBackend.findConversation(account, jid);
1578			if (conversation != null) {
1579				conversation.setStatus(Conversation.STATUS_AVAILABLE);
1580				conversation.setAccount(account);
1581				if (muc) {
1582					conversation.setMode(Conversation.MODE_MULTI);
1583					conversation.setContactJid(jid);
1584				} else {
1585					conversation.setMode(Conversation.MODE_SINGLE);
1586					conversation.setContactJid(jid.toBareJid());
1587				}
1588				conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1589				this.databaseBackend.updateConversation(conversation);
1590			} else {
1591				String conversationName;
1592				Contact contact = account.getRoster().getContact(jid);
1593				if (contact != null) {
1594					conversationName = contact.getDisplayName();
1595				} else {
1596					conversationName = jid.getLocalpart();
1597				}
1598				if (muc) {
1599					conversation = new Conversation(conversationName, account, jid,
1600							Conversation.MODE_MULTI);
1601				} else {
1602					conversation = new Conversation(conversationName, account, jid.toBareJid(),
1603							Conversation.MODE_SINGLE);
1604				}
1605				this.databaseBackend.createConversation(conversation);
1606			}
1607			if (account.getXmppConnection() != null
1608					&& account.getXmppConnection().getFeatures().mam()
1609					&& !muc) {
1610				if (query == null) {
1611					this.mMessageArchiveService.query(conversation);
1612				} else {
1613					if (query.getConversation() == null) {
1614						this.mMessageArchiveService.query(conversation, query.getStart());
1615					}
1616				}
1617			}
1618			checkDeletedFiles(conversation);
1619			this.conversations.add(conversation);
1620			updateConversationUi();
1621			return conversation;
1622		}
1623	}
1624
1625	public void archiveConversation(Conversation conversation) {
1626		getNotificationService().clear(conversation);
1627		conversation.setStatus(Conversation.STATUS_ARCHIVED);
1628		synchronized (this.conversations) {
1629			if (conversation.getMode() == Conversation.MODE_MULTI) {
1630				if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1631					Bookmark bookmark = conversation.getBookmark();
1632					if (bookmark != null && bookmark.autojoin() && respectAutojoin()) {
1633						bookmark.setAutojoin(false);
1634						pushBookmarks(bookmark.getAccount());
1635					}
1636				}
1637				leaveMuc(conversation);
1638			} else {
1639				conversation.endOtrIfNeeded();
1640				if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1641					Log.d(Config.LOGTAG, "Canceling presence request from " + conversation.getJid().toString());
1642					sendPresencePacket(
1643							conversation.getAccount(),
1644							mPresenceGenerator.stopPresenceUpdatesTo(conversation.getContact())
1645					);
1646				}
1647			}
1648			updateConversation(conversation);
1649			this.conversations.remove(conversation);
1650			updateConversationUi();
1651		}
1652	}
1653
1654	public void createAccount(final Account account) {
1655		account.initAccountServices(this);
1656		databaseBackend.createAccount(account);
1657		this.accounts.add(account);
1658		this.reconnectAccountInBackground(account);
1659		updateAccountUi();
1660	}
1661
1662	public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
1663		new Thread(new Runnable() {
1664			@Override
1665			public void run() {
1666				try {
1667					X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
1668					Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
1669					if (findAccountByJid(info.first) == null) {
1670						Account account = new Account(info.first, "");
1671						account.setPrivateKeyAlias(alias);
1672						account.setOption(Account.OPTION_DISABLED, true);
1673						account.setDisplayName(info.second);
1674						createAccount(account);
1675						callback.onAccountCreated(account);
1676						if (Config.X509_VERIFICATION) {
1677							try {
1678								getMemorizingTrustManager().getNonInteractive(account.getJid().getDomainpart()).checkClientTrusted(chain, "RSA");
1679							} catch (CertificateException e) {
1680								callback.informUser(R.string.certificate_chain_is_not_trusted);
1681							}
1682						}
1683					} else {
1684						callback.informUser(R.string.account_already_exists);
1685					}
1686				} catch (Exception e) {
1687					e.printStackTrace();
1688					callback.informUser(R.string.unable_to_parse_certificate);
1689				}
1690			}
1691		}).start();
1692
1693	}
1694
1695	public void updateKeyInAccount(final Account account, final String alias) {
1696		Log.d(Config.LOGTAG, "update key in account " + alias);
1697		try {
1698			X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
1699			Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
1700			if (account.getJid().toBareJid().equals(info.first)) {
1701				account.setPrivateKeyAlias(alias);
1702				account.setDisplayName(info.second);
1703				databaseBackend.updateAccount(account);
1704				if (Config.X509_VERIFICATION) {
1705					try {
1706						getMemorizingTrustManager().getNonInteractive(account.getJid().getDomainpart()).checkClientTrusted(chain, "RSA");
1707					} catch (CertificateException e) {
1708						showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
1709					}
1710					account.getAxolotlService().regenerateKeys(true);
1711				}
1712			} else {
1713				showErrorToastInUi(R.string.jid_does_not_match_certificate);
1714			}
1715		} catch (Exception e) {
1716			e.printStackTrace();
1717		}
1718	}
1719
1720	public boolean updateAccount(final Account account) {
1721		if (databaseBackend.updateAccount(account)) {
1722			account.setShowErrorNotification(true);
1723			this.statusListener.onStatusChanged(account);
1724			databaseBackend.updateAccount(account);
1725			reconnectAccountInBackground(account);
1726			updateAccountUi();
1727			getNotificationService().updateErrorNotification();
1728			return true;
1729		} else {
1730			return false;
1731		}
1732	}
1733
1734	public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
1735		final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
1736		sendIqPacket(account, iq, new OnIqPacketReceived() {
1737			@Override
1738			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1739				if (packet.getType() == IqPacket.TYPE.RESULT) {
1740					account.setPassword(newPassword);
1741					account.setOption(Account.OPTION_MAGIC_CREATE, false);
1742					databaseBackend.updateAccount(account);
1743					callback.onPasswordChangeSucceeded();
1744				} else {
1745					callback.onPasswordChangeFailed();
1746				}
1747			}
1748		});
1749	}
1750
1751	public void deleteAccount(final Account account) {
1752		synchronized (this.conversations) {
1753			for (final Conversation conversation : conversations) {
1754				if (conversation.getAccount() == account) {
1755					if (conversation.getMode() == Conversation.MODE_MULTI) {
1756						leaveMuc(conversation);
1757					} else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1758						conversation.endOtrIfNeeded();
1759					}
1760					conversations.remove(conversation);
1761				}
1762			}
1763			if (account.getXmppConnection() != null) {
1764				new Thread(new Runnable() {
1765					@Override
1766					public void run() {
1767						disconnect(account, true);
1768					}
1769				}).start();
1770			}
1771			Runnable runnable = new Runnable() {
1772				@Override
1773				public void run() {
1774					if (!databaseBackend.deleteAccount(account)) {
1775						Log.d(Config.LOGTAG,account.getJid().toBareJid()+": unable to delete account");
1776					}
1777				}
1778			};
1779			mDatabaseExecutor.execute(runnable);
1780			this.accounts.remove(account);
1781			updateAccountUi();
1782			getNotificationService().updateErrorNotification();
1783		}
1784	}
1785
1786	public void setOnConversationListChangedListener(OnConversationUpdate listener) {
1787		synchronized (this) {
1788			this.mLastActivity = System.currentTimeMillis();
1789			if (checkListeners()) {
1790				switchToForeground();
1791			}
1792			this.mOnConversationUpdate = listener;
1793			this.mNotificationService.setIsInForeground(true);
1794			if (this.convChangedListenerCount < 2) {
1795				this.convChangedListenerCount++;
1796			}
1797		}
1798	}
1799
1800	public void removeOnConversationListChangedListener() {
1801		synchronized (this) {
1802			this.convChangedListenerCount--;
1803			if (this.convChangedListenerCount <= 0) {
1804				this.convChangedListenerCount = 0;
1805				this.mOnConversationUpdate = null;
1806				this.mNotificationService.setIsInForeground(false);
1807				if (checkListeners()) {
1808					switchToBackground();
1809				}
1810			}
1811		}
1812	}
1813
1814	public void setOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
1815		synchronized (this) {
1816			if (checkListeners()) {
1817				switchToForeground();
1818			}
1819			this.mOnShowErrorToast = onShowErrorToast;
1820			if (this.showErrorToastListenerCount < 2) {
1821				this.showErrorToastListenerCount++;
1822			}
1823		}
1824		this.mOnShowErrorToast = onShowErrorToast;
1825	}
1826
1827	public void removeOnShowErrorToastListener() {
1828		synchronized (this) {
1829			this.showErrorToastListenerCount--;
1830			if (this.showErrorToastListenerCount <= 0) {
1831				this.showErrorToastListenerCount = 0;
1832				this.mOnShowErrorToast = null;
1833				if (checkListeners()) {
1834					switchToBackground();
1835				}
1836			}
1837		}
1838	}
1839
1840	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1841		synchronized (this) {
1842			if (checkListeners()) {
1843				switchToForeground();
1844			}
1845			this.mOnAccountUpdate = listener;
1846			if (this.accountChangedListenerCount < 2) {
1847				this.accountChangedListenerCount++;
1848			}
1849		}
1850	}
1851
1852	public void removeOnAccountListChangedListener() {
1853		synchronized (this) {
1854			this.accountChangedListenerCount--;
1855			if (this.accountChangedListenerCount <= 0) {
1856				this.mOnAccountUpdate = null;
1857				this.accountChangedListenerCount = 0;
1858				if (checkListeners()) {
1859					switchToBackground();
1860				}
1861			}
1862		}
1863	}
1864
1865	public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
1866		synchronized (this) {
1867			if (checkListeners()) {
1868				switchToForeground();
1869			}
1870			this.mOnCaptchaRequested = listener;
1871			if (this.captchaRequestedListenerCount < 2) {
1872				this.captchaRequestedListenerCount++;
1873			}
1874		}
1875	}
1876
1877	public void removeOnCaptchaRequestedListener() {
1878		synchronized (this) {
1879			this.captchaRequestedListenerCount--;
1880			if (this.captchaRequestedListenerCount <= 0) {
1881				this.mOnCaptchaRequested = null;
1882				this.captchaRequestedListenerCount = 0;
1883				if (checkListeners()) {
1884					switchToBackground();
1885				}
1886			}
1887		}
1888	}
1889
1890	public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
1891		synchronized (this) {
1892			if (checkListeners()) {
1893				switchToForeground();
1894			}
1895			this.mOnRosterUpdate = listener;
1896			if (this.rosterChangedListenerCount < 2) {
1897				this.rosterChangedListenerCount++;
1898			}
1899		}
1900	}
1901
1902	public void removeOnRosterUpdateListener() {
1903		synchronized (this) {
1904			this.rosterChangedListenerCount--;
1905			if (this.rosterChangedListenerCount <= 0) {
1906				this.rosterChangedListenerCount = 0;
1907				this.mOnRosterUpdate = null;
1908				if (checkListeners()) {
1909					switchToBackground();
1910				}
1911			}
1912		}
1913	}
1914
1915	public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
1916		synchronized (this) {
1917			if (checkListeners()) {
1918				switchToForeground();
1919			}
1920			this.mOnUpdateBlocklist = listener;
1921			if (this.updateBlocklistListenerCount < 2) {
1922				this.updateBlocklistListenerCount++;
1923			}
1924		}
1925	}
1926
1927	public void removeOnUpdateBlocklistListener() {
1928		synchronized (this) {
1929			this.updateBlocklistListenerCount--;
1930			if (this.updateBlocklistListenerCount <= 0) {
1931				this.updateBlocklistListenerCount = 0;
1932				this.mOnUpdateBlocklist = null;
1933				if (checkListeners()) {
1934					switchToBackground();
1935				}
1936			}
1937		}
1938	}
1939
1940	public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
1941		synchronized (this) {
1942			if (checkListeners()) {
1943				switchToForeground();
1944			}
1945			this.mOnKeyStatusUpdated = listener;
1946			if (this.keyStatusUpdatedListenerCount < 2) {
1947				this.keyStatusUpdatedListenerCount++;
1948			}
1949		}
1950	}
1951
1952	public void removeOnNewKeysAvailableListener() {
1953		synchronized (this) {
1954			this.keyStatusUpdatedListenerCount--;
1955			if (this.keyStatusUpdatedListenerCount <= 0) {
1956				this.keyStatusUpdatedListenerCount = 0;
1957				this.mOnKeyStatusUpdated = null;
1958				if (checkListeners()) {
1959					switchToBackground();
1960				}
1961			}
1962		}
1963	}
1964
1965	public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
1966		synchronized (this) {
1967			if (checkListeners()) {
1968				switchToForeground();
1969			}
1970			this.mOnMucRosterUpdate = listener;
1971			if (this.mucRosterChangedListenerCount < 2) {
1972				this.mucRosterChangedListenerCount++;
1973			}
1974		}
1975	}
1976
1977	public void removeOnMucRosterUpdateListener() {
1978		synchronized (this) {
1979			this.mucRosterChangedListenerCount--;
1980			if (this.mucRosterChangedListenerCount <= 0) {
1981				this.mucRosterChangedListenerCount = 0;
1982				this.mOnMucRosterUpdate = null;
1983				if (checkListeners()) {
1984					switchToBackground();
1985				}
1986			}
1987		}
1988	}
1989
1990	public boolean checkListeners() {
1991		return (this.mOnAccountUpdate == null
1992				&& this.mOnConversationUpdate == null
1993				&& this.mOnRosterUpdate == null
1994				&& this.mOnCaptchaRequested == null
1995				&& this.mOnUpdateBlocklist == null
1996				&& this.mOnShowErrorToast == null
1997				&& this.mOnKeyStatusUpdated == null);
1998	}
1999
2000	private void switchToForeground() {
2001		final boolean broadcastLastActivity = broadcastLastActivity();
2002		for (Conversation conversation : getConversations()) {
2003			conversation.setIncomingChatState(ChatState.ACTIVE);
2004		}
2005		for (Account account : getAccounts()) {
2006			if (account.getStatus() == Account.State.ONLINE) {
2007				account.deactivateGracePeriod();
2008				final XmppConnection connection = account.getXmppConnection();
2009				if (connection != null ) {
2010					if (connection.getFeatures().csi()) {
2011						connection.sendActive();
2012					}
2013					if (broadcastLastActivity) {
2014						sendPresence(account, false); //send new presence but don't include idle because we are not
2015					}
2016				}
2017			}
2018		}
2019		Log.d(Config.LOGTAG, "app switched into foreground");
2020	}
2021
2022	private void switchToBackground() {
2023		final boolean broadcastLastActivity = broadcastLastActivity();
2024		for (Account account : getAccounts()) {
2025			if (account.getStatus() == Account.State.ONLINE) {
2026				XmppConnection connection = account.getXmppConnection();
2027				if (connection != null) {
2028					if (broadcastLastActivity) {
2029						sendPresence(account, broadcastLastActivity);
2030					}
2031					if (connection.getFeatures().csi()) {
2032						connection.sendInactive();
2033					}
2034				}
2035			}
2036		}
2037		this.mNotificationService.setIsInForeground(false);
2038		Log.d(Config.LOGTAG, "app switched into background");
2039	}
2040
2041	private void connectMultiModeConversations(Account account) {
2042		List<Conversation> conversations = getConversations();
2043		for (Conversation conversation : conversations) {
2044			if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) {
2045				joinMuc(conversation);
2046			}
2047		}
2048	}
2049
2050	public void joinMuc(Conversation conversation) {
2051		joinMuc(conversation,null, false);
2052	}
2053
2054	public void joinMuc(Conversation conversation, boolean followedInvite) {
2055		joinMuc(conversation, null, followedInvite);
2056	}
2057
2058	private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
2059		joinMuc(conversation,onConferenceJoined,false);
2060	}
2061
2062	private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) {
2063		Account account = conversation.getAccount();
2064		account.pendingConferenceJoins.remove(conversation);
2065		account.pendingConferenceLeaves.remove(conversation);
2066		if (account.getStatus() == Account.State.ONLINE) {
2067			conversation.resetMucOptions();
2068			if (onConferenceJoined != null) {
2069				conversation.getMucOptions().flagNoAutoPushConfiguration();
2070			}
2071			conversation.setHasMessagesLeftOnServer(false);
2072			fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() {
2073
2074				private void join(Conversation conversation) {
2075					Account account = conversation.getAccount();
2076					final MucOptions mucOptions = conversation.getMucOptions();
2077					final Jid joinJid = mucOptions.getSelf().getFullJid();
2078					Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
2079					PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous());
2080					packet.setTo(joinJid);
2081					Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
2082					if (conversation.getMucOptions().getPassword() != null) {
2083						x.addChild("password").setContent(mucOptions.getPassword());
2084					}
2085
2086					if (mucOptions.mamSupport()) {
2087						// Use MAM instead of the limited muc history to get history
2088						x.addChild("history").setAttribute("maxchars", "0");
2089					} else {
2090						// Fallback to muc history
2091						x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted()));
2092					}
2093					sendPresencePacket(account, packet);
2094					if (onConferenceJoined != null) {
2095						onConferenceJoined.onConferenceJoined(conversation);
2096					}
2097					if (!joinJid.equals(conversation.getJid())) {
2098						conversation.setContactJid(joinJid);
2099						databaseBackend.updateConversation(conversation);
2100					}
2101
2102					if (mucOptions.mamSupport()) {
2103						getMessageArchiveService().catchupMUC(conversation);
2104					}
2105					if (mucOptions.membersOnly() && mucOptions.nonanonymous()) {
2106						fetchConferenceMembers(conversation);
2107						if (followedInvite && conversation.getBookmark() == null) {
2108							saveConversationAsBookmark(conversation,null);
2109						}
2110					}
2111					sendUnsentMessages(conversation);
2112				}
2113
2114				@Override
2115				public void onConferenceConfigurationFetched(Conversation conversation) {
2116					join(conversation);
2117				}
2118
2119				@Override
2120				public void onFetchFailed(final Conversation conversation, Element error) {
2121					if (error != null && "remote-server-not-found".equals(error.getName())) {
2122						conversation.getMucOptions().setError(MucOptions.Error.SERVER_NOT_FOUND);
2123					} else {
2124						join(conversation);
2125						fetchConferenceConfiguration(conversation);
2126					}
2127				}
2128			});
2129			updateConversationUi();
2130		} else {
2131			account.pendingConferenceJoins.add(conversation);
2132			conversation.resetMucOptions();
2133			conversation.setHasMessagesLeftOnServer(false);
2134			updateConversationUi();
2135		}
2136	}
2137
2138	private void fetchConferenceMembers(final Conversation conversation) {
2139		final Account account = conversation.getAccount();
2140		final String[] affiliations = {"member","admin","owner"};
2141		OnIqPacketReceived callback = new OnIqPacketReceived() {
2142
2143			private int i = 0;
2144			private boolean success = true;
2145
2146			@Override
2147			public void onIqPacketReceived(Account account, IqPacket packet) {
2148
2149				Element query = packet.query("http://jabber.org/protocol/muc#admin");
2150				if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
2151					for(Element child : query.getChildren()) {
2152						if ("item".equals(child.getName())) {
2153							MucOptions.User user = AbstractParser.parseItem(conversation,child);
2154							if (!user.realJidMatchesAccount()) {
2155								conversation.getMucOptions().updateUser(user);
2156							}
2157						}
2158					}
2159				} else {
2160					success = false;
2161					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not request affiliation "+affiliations[i]+" in "+conversation.getJid().toBareJid());
2162				}
2163				++i;
2164				if (i >= affiliations.length) {
2165					List<Jid> members = conversation.getMucOptions().getMembers();
2166					if (success) {
2167						List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
2168						boolean changed = false;
2169						for(ListIterator<Jid> iterator = cryptoTargets.listIterator(); iterator.hasNext();) {
2170							Jid jid = iterator.next();
2171							if (!members.contains(jid)) {
2172								iterator.remove();
2173								Log.d(Config.LOGTAG,account.getJid().toBareJid()+": removed "+jid+" from crypto targets of "+conversation.getName());
2174								changed = true;
2175							}
2176						}
2177						if (changed) {
2178							conversation.setAcceptedCryptoTargets(cryptoTargets);
2179							updateConversation(conversation);
2180						}
2181					}
2182					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved members for "+conversation.getJid().toBareJid()+": "+conversation.getMucOptions().getMembers());
2183					getAvatarService().clear(conversation);
2184					updateMucRosterUi();
2185					updateConversationUi();
2186				}
2187			}
2188		};
2189		for(String affiliation : affiliations) {
2190			sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
2191		}
2192		Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching members for "+conversation.getName());
2193	}
2194
2195	public void providePasswordForMuc(Conversation conversation, String password) {
2196		if (conversation.getMode() == Conversation.MODE_MULTI) {
2197			conversation.getMucOptions().setPassword(password);
2198			if (conversation.getBookmark() != null) {
2199				if (respectAutojoin()) {
2200					conversation.getBookmark().setAutojoin(true);
2201				}
2202				pushBookmarks(conversation.getAccount());
2203			}
2204			updateConversation(conversation);
2205			joinMuc(conversation);
2206		}
2207	}
2208
2209	public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
2210		final MucOptions options = conversation.getMucOptions();
2211		final Jid joinJid = options.createJoinJid(nick);
2212		if (options.online()) {
2213			Account account = conversation.getAccount();
2214			options.setOnRenameListener(new OnRenameListener() {
2215
2216				@Override
2217				public void onSuccess() {
2218					conversation.setContactJid(joinJid);
2219					databaseBackend.updateConversation(conversation);
2220					Bookmark bookmark = conversation.getBookmark();
2221					if (bookmark != null) {
2222						bookmark.setNick(nick);
2223						pushBookmarks(bookmark.getAccount());
2224					}
2225					callback.success(conversation);
2226				}
2227
2228				@Override
2229				public void onFailure() {
2230					callback.error(R.string.nick_in_use, conversation);
2231				}
2232			});
2233
2234			PresencePacket packet = new PresencePacket();
2235			packet.setTo(joinJid);
2236			packet.setFrom(conversation.getAccount().getJid());
2237
2238			String sig = account.getPgpSignature();
2239			if (sig != null) {
2240				packet.addChild("status").setContent("online");
2241				packet.addChild("x", "jabber:x:signed").setContent(sig);
2242			}
2243			sendPresencePacket(account, packet);
2244		} else {
2245			conversation.setContactJid(joinJid);
2246			databaseBackend.updateConversation(conversation);
2247			if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
2248				Bookmark bookmark = conversation.getBookmark();
2249				if (bookmark != null) {
2250					bookmark.setNick(nick);
2251					pushBookmarks(bookmark.getAccount());
2252				}
2253				joinMuc(conversation);
2254			}
2255		}
2256	}
2257
2258	public void leaveMuc(Conversation conversation) {
2259		leaveMuc(conversation, false);
2260	}
2261
2262	private void leaveMuc(Conversation conversation, boolean now) {
2263		Account account = conversation.getAccount();
2264		account.pendingConferenceJoins.remove(conversation);
2265		account.pendingConferenceLeaves.remove(conversation);
2266		if (account.getStatus() == Account.State.ONLINE || now) {
2267			PresencePacket packet = new PresencePacket();
2268			packet.setTo(conversation.getMucOptions().getSelf().getFullJid());
2269			packet.setFrom(conversation.getAccount().getJid());
2270			packet.setAttribute("type", "unavailable");
2271			sendPresencePacket(conversation.getAccount(), packet);
2272			conversation.getMucOptions().setOffline();
2273			conversation.deregisterWithBookmark();
2274			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
2275					+ ": leaving muc " + conversation.getJid());
2276		} else {
2277			account.pendingConferenceLeaves.add(conversation);
2278		}
2279	}
2280
2281	private String findConferenceServer(final Account account) {
2282		String server;
2283		if (account.getXmppConnection() != null) {
2284			server = account.getXmppConnection().getMucServer();
2285			if (server != null) {
2286				return server;
2287			}
2288		}
2289		for (Account other : getAccounts()) {
2290			if (other != account && other.getXmppConnection() != null) {
2291				server = other.getXmppConnection().getMucServer();
2292				if (server != null) {
2293					return server;
2294				}
2295			}
2296		}
2297		return null;
2298	}
2299
2300	public void createAdhocConference(final Account account,
2301									  final String subject,
2302									  final Iterable<Jid> jids,
2303									  final UiCallback<Conversation> callback) {
2304		Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString());
2305		if (account.getStatus() == Account.State.ONLINE) {
2306			try {
2307				String server = findConferenceServer(account);
2308				if (server == null) {
2309					if (callback != null) {
2310						callback.error(R.string.no_conference_server_found, null);
2311					}
2312					return;
2313				}
2314				final Jid jid = Jid.fromParts(new BigInteger(64, getRNG()).toString(Character.MAX_RADIX), server, null);
2315				final Conversation conversation = findOrCreateConversation(account, jid, true);
2316				joinMuc(conversation, new OnConferenceJoined() {
2317					@Override
2318					public void onConferenceJoined(final Conversation conversation) {
2319						pushConferenceConfiguration(conversation, IqGenerator.defaultRoomConfiguration(), new OnConferenceOptionsPushed() {
2320							@Override
2321							public void onPushSucceeded() {
2322								if (subject != null && !subject.trim().isEmpty()) {
2323									pushSubjectToConference(conversation, subject.trim());
2324								}
2325								for (Jid invite : jids) {
2326									invite(conversation, invite);
2327								}
2328								if (account.countPresences() > 1) {
2329									directInvite(conversation, account.getJid().toBareJid());
2330								}
2331								saveConversationAsBookmark(conversation, subject);
2332								if (callback != null) {
2333									callback.success(conversation);
2334								}
2335							}
2336
2337							@Override
2338							public void onPushFailed() {
2339								archiveConversation(conversation);
2340								if (callback != null) {
2341									callback.error(R.string.conference_creation_failed, conversation);
2342								}
2343							}
2344						});
2345					}
2346				});
2347			} catch (InvalidJidException e) {
2348				if (callback != null) {
2349					callback.error(R.string.conference_creation_failed, null);
2350				}
2351			}
2352		} else {
2353			if (callback != null) {
2354				callback.error(R.string.not_connected_try_again, null);
2355			}
2356		}
2357	}
2358
2359	public void fetchConferenceConfiguration(final Conversation conversation) {
2360		fetchConferenceConfiguration(conversation, null);
2361	}
2362
2363	public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
2364		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2365		request.setTo(conversation.getJid().toBareJid());
2366		request.query("http://jabber.org/protocol/disco#info");
2367		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2368			@Override
2369			public void onIqPacketReceived(Account account, IqPacket packet) {
2370				Element query = packet.findChild("query","http://jabber.org/protocol/disco#info");
2371				if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
2372					ArrayList<String> features = new ArrayList<>();
2373					for (Element child : query.getChildren()) {
2374						if (child != null && child.getName().equals("feature")) {
2375							String var = child.getAttribute("var");
2376							if (var != null) {
2377								features.add(var);
2378							}
2379						}
2380					}
2381					Element form = query.findChild("x", "jabber:x:data");
2382					if (form != null) {
2383						conversation.getMucOptions().updateFormData(Data.parse(form));
2384					}
2385					conversation.getMucOptions().updateFeatures(features);
2386					if (callback != null) {
2387						callback.onConferenceConfigurationFetched(conversation);
2388					}
2389					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetched muc configuration for "+conversation.getJid().toBareJid()+" - "+features.toString());
2390					updateConversationUi();
2391				} else if (packet.getType() == IqPacket.TYPE.ERROR) {
2392					if (callback != null) {
2393						callback.onFetchFailed(conversation, packet.getError());
2394					}
2395				}
2396			}
2397		});
2398	}
2399
2400	public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) {
2401		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2402		request.setTo(conversation.getJid().toBareJid());
2403		request.query("http://jabber.org/protocol/muc#owner");
2404		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2405			@Override
2406			public void onIqPacketReceived(Account account, IqPacket packet) {
2407				if (packet.getType() == IqPacket.TYPE.RESULT) {
2408					Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
2409					for (Field field : data.getFields()) {
2410						if (options.containsKey(field.getFieldName())) {
2411							field.setValue(options.getString(field.getFieldName()));
2412						}
2413					}
2414					data.submit();
2415					IqPacket set = new IqPacket(IqPacket.TYPE.SET);
2416					set.setTo(conversation.getJid().toBareJid());
2417					set.query("http://jabber.org/protocol/muc#owner").addChild(data);
2418					sendIqPacket(account, set, new OnIqPacketReceived() {
2419						@Override
2420						public void onIqPacketReceived(Account account, IqPacket packet) {
2421							if (callback != null) {
2422								if (packet.getType() == IqPacket.TYPE.RESULT) {
2423									callback.onPushSucceeded();
2424								} else {
2425									callback.onPushFailed();
2426								}
2427							}
2428						}
2429					});
2430				} else {
2431					if (callback != null) {
2432						callback.onPushFailed();
2433					}
2434				}
2435			}
2436		});
2437	}
2438
2439	public void pushSubjectToConference(final Conversation conference, final String subject) {
2440		MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject);
2441		this.sendMessagePacket(conference.getAccount(), packet);
2442		final MucOptions mucOptions = conference.getMucOptions();
2443		final MucOptions.User self = mucOptions.getSelf();
2444		if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
2445			Bundle options = new Bundle();
2446			options.putString("muc#roomconfig_persistentroom", "1");
2447			this.pushConferenceConfiguration(conference, options, null);
2448		}
2449	}
2450
2451	public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
2452		final Jid jid = user.toBareJid();
2453		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
2454		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
2455			@Override
2456			public void onIqPacketReceived(Account account, IqPacket packet) {
2457				if (packet.getType() == IqPacket.TYPE.RESULT) {
2458					conference.getMucOptions().changeAffiliation(jid, affiliation);
2459					getAvatarService().clear(conference);
2460					callback.onAffiliationChangedSuccessful(jid);
2461				} else {
2462					callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
2463				}
2464			}
2465		});
2466	}
2467
2468	public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
2469		List<Jid> jids = new ArrayList<>();
2470		for (MucOptions.User user : conference.getMucOptions().getUsers()) {
2471			if (user.getAffiliation() == before && user.getRealJid() != null) {
2472				jids.add(user.getRealJid());
2473			}
2474		}
2475		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
2476		sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
2477	}
2478
2479	public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
2480		IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
2481		Log.d(Config.LOGTAG, request.toString());
2482		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
2483			@Override
2484			public void onIqPacketReceived(Account account, IqPacket packet) {
2485				Log.d(Config.LOGTAG, packet.toString());
2486				if (packet.getType() == IqPacket.TYPE.RESULT) {
2487					callback.onRoleChangedSuccessful(nick);
2488				} else {
2489					callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
2490				}
2491			}
2492		});
2493	}
2494
2495	private void disconnect(Account account, boolean force) {
2496		if ((account.getStatus() == Account.State.ONLINE)
2497				|| (account.getStatus() == Account.State.DISABLED)) {
2498			final XmppConnection connection = account.getXmppConnection();
2499			if (!force) {
2500				List<Conversation> conversations = getConversations();
2501				for (Conversation conversation : conversations) {
2502					if (conversation.getAccount() == account) {
2503						if (conversation.getMode() == Conversation.MODE_MULTI) {
2504							leaveMuc(conversation, true);
2505						} else {
2506							if (conversation.endOtrIfNeeded()) {
2507								Log.d(Config.LOGTAG, account.getJid().toBareJid()
2508										+ ": ended otr session with "
2509										+ conversation.getJid());
2510							}
2511						}
2512					}
2513				}
2514				sendOfflinePresence(account);
2515			}
2516			connection.disconnect(force);
2517		}
2518	}
2519
2520	@Override
2521	public IBinder onBind(Intent intent) {
2522		return mBinder;
2523	}
2524
2525	public void updateMessage(Message message) {
2526		databaseBackend.updateMessage(message);
2527		updateConversationUi();
2528	}
2529
2530	public void updateMessage(Message message, String uuid) {
2531		databaseBackend.updateMessage(message, uuid);
2532		updateConversationUi();
2533	}
2534
2535	protected void syncDirtyContacts(Account account) {
2536		for (Contact contact : account.getRoster().getContacts()) {
2537			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
2538				pushContactToServer(contact);
2539			}
2540			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
2541				deleteContactOnServer(contact);
2542			}
2543		}
2544	}
2545
2546	public void createContact(Contact contact) {
2547		boolean autoGrant = getPreferences().getBoolean("grant_new_contacts", true);
2548		if (autoGrant) {
2549			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
2550			contact.setOption(Contact.Options.ASKING);
2551		}
2552		pushContactToServer(contact);
2553	}
2554
2555	public void onOtrSessionEstablished(Conversation conversation) {
2556		final Account account = conversation.getAccount();
2557		final Session otrSession = conversation.getOtrSession();
2558		Log.d(Config.LOGTAG,
2559				account.getJid().toBareJid() + " otr session established with "
2560						+ conversation.getJid() + "/"
2561						+ otrSession.getSessionID().getUserID());
2562		conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
2563
2564			@Override
2565			public void onMessageFound(Message message) {
2566				SessionID id = otrSession.getSessionID();
2567				try {
2568					message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
2569				} catch (InvalidJidException e) {
2570					return;
2571				}
2572				if (message.needsUploading()) {
2573					mJingleConnectionManager.createNewConnection(message);
2574				} else {
2575					MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
2576					if (outPacket != null) {
2577						mMessageGenerator.addDelay(outPacket, message.getTimeSent());
2578						message.setStatus(Message.STATUS_SEND);
2579						databaseBackend.updateMessage(message);
2580						sendMessagePacket(account, outPacket);
2581					}
2582				}
2583				updateConversationUi();
2584			}
2585		});
2586	}
2587
2588	public boolean renewSymmetricKey(Conversation conversation) {
2589		Account account = conversation.getAccount();
2590		byte[] symmetricKey = new byte[32];
2591		this.mRandom.nextBytes(symmetricKey);
2592		Session otrSession = conversation.getOtrSession();
2593		if (otrSession != null) {
2594			MessagePacket packet = new MessagePacket();
2595			packet.setType(MessagePacket.TYPE_CHAT);
2596			packet.setFrom(account.getJid());
2597			MessageGenerator.addMessageHints(packet);
2598			packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
2599					+ otrSession.getSessionID().getUserID());
2600			try {
2601				packet.setBody(otrSession
2602						.transformSending(CryptoHelper.FILETRANSFER
2603								+ CryptoHelper.bytesToHex(symmetricKey))[0]);
2604				sendMessagePacket(account, packet);
2605				conversation.setSymmetricKey(symmetricKey);
2606				return true;
2607			} catch (OtrException e) {
2608				return false;
2609			}
2610		}
2611		return false;
2612	}
2613
2614	public void pushContactToServer(final Contact contact) {
2615		contact.resetOption(Contact.Options.DIRTY_DELETE);
2616		contact.setOption(Contact.Options.DIRTY_PUSH);
2617		final Account account = contact.getAccount();
2618		if (account.getStatus() == Account.State.ONLINE) {
2619			final boolean ask = contact.getOption(Contact.Options.ASKING);
2620			final boolean sendUpdates = contact
2621					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
2622					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
2623			final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2624			iq.query(Xmlns.ROSTER).addChild(contact.asElement());
2625			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2626			if (sendUpdates) {
2627				sendPresencePacket(account,
2628						mPresenceGenerator.sendPresenceUpdatesTo(contact));
2629			}
2630			if (ask) {
2631				sendPresencePacket(account,
2632						mPresenceGenerator.requestPresenceUpdatesFrom(contact));
2633			}
2634		}
2635	}
2636
2637	public void publishAvatar(Account account, Uri image, UiCallback<Avatar> callback) {
2638		final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
2639		final int size = Config.AVATAR_SIZE;
2640		final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
2641		if (avatar != null) {
2642			avatar.height = size;
2643			avatar.width = size;
2644			if (format.equals(Bitmap.CompressFormat.WEBP)) {
2645				avatar.type = "image/webp";
2646			} else if (format.equals(Bitmap.CompressFormat.JPEG)) {
2647				avatar.type = "image/jpeg";
2648			} else if (format.equals(Bitmap.CompressFormat.PNG)) {
2649				avatar.type = "image/png";
2650			}
2651			if (!getFileBackend().save(avatar)) {
2652				callback.error(R.string.error_saving_avatar, avatar);
2653				return;
2654			}
2655			publishAvatar(account, avatar, callback);
2656		} else {
2657			callback.error(R.string.error_publish_avatar_converting, null);
2658		}
2659	}
2660
2661	public void publishAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2662		IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
2663		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2664
2665			@Override
2666			public void onIqPacketReceived(Account account, IqPacket result) {
2667				if (result.getType() == IqPacket.TYPE.RESULT) {
2668					final IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar);
2669					sendIqPacket(account, packet, new OnIqPacketReceived() {
2670						@Override
2671						public void onIqPacketReceived(Account account, IqPacket result) {
2672							if (result.getType() == IqPacket.TYPE.RESULT) {
2673								if (account.setAvatar(avatar.getFilename())) {
2674									getAvatarService().clear(account);
2675									databaseBackend.updateAccount(account);
2676								}
2677								Log.d(Config.LOGTAG,account.getJid().toBareJid()+": published avatar "+(avatar.size/1024)+"KiB");
2678								if (callback != null) {
2679									callback.success(avatar);
2680								}
2681							} else {
2682								if (callback != null) {
2683									callback.error(R.string.error_publish_avatar_server_reject,avatar);
2684								}
2685							}
2686						}
2687					});
2688				} else {
2689					Element error = result.findChild("error");
2690					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server rejected avatar "+(avatar.size/1024)+"KiB "+(error!=null?error.toString():""));
2691					if (callback != null) {
2692						callback.error(R.string.error_publish_avatar_server_reject, avatar);
2693					}
2694				}
2695			}
2696		});
2697	}
2698
2699	public void republishAvatarIfNeeded(Account account) {
2700		if (account.getAxolotlService().isPepBroken()) {
2701			Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping republication of avatar because pep is broken");
2702			return;
2703		}
2704		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
2705		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2706
2707			private Avatar parseAvatar(IqPacket packet) {
2708				Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
2709				if (pubsub != null) {
2710					Element items = pubsub.findChild("items");
2711					if (items != null) {
2712						return Avatar.parseMetadata(items);
2713					}
2714				}
2715				return null;
2716			}
2717
2718			private boolean errorIsItemNotFound(IqPacket packet) {
2719				Element error = packet.findChild("error");
2720				return packet.getType() == IqPacket.TYPE.ERROR
2721						&& error != null
2722						&& error.hasChild("item-not-found");
2723			}
2724
2725			@Override
2726			public void onIqPacketReceived(Account account, IqPacket packet) {
2727				if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) {
2728					Avatar serverAvatar = parseAvatar(packet);
2729					if (serverAvatar == null && account.getAvatar() != null) {
2730						Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar());
2731						if (avatar != null) {
2732							Log.d(Config.LOGTAG,account.getJid().toBareJid()+": avatar on server was null. republishing");
2733							publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null);
2734						} else {
2735							Log.e(Config.LOGTAG, account.getJid().toBareJid()+": error rereading avatar");
2736						}
2737					}
2738				}
2739			}
2740		});
2741	}
2742
2743	public void fetchAvatar(Account account, Avatar avatar) {
2744		fetchAvatar(account, avatar, null);
2745	}
2746
2747	public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2748		final String KEY = generateFetchKey(account, avatar);
2749		synchronized (this.mInProgressAvatarFetches) {
2750			if (!this.mInProgressAvatarFetches.contains(KEY)) {
2751				switch (avatar.origin) {
2752					case PEP:
2753						this.mInProgressAvatarFetches.add(KEY);
2754						fetchAvatarPep(account, avatar, callback);
2755						break;
2756					case VCARD:
2757						this.mInProgressAvatarFetches.add(KEY);
2758						fetchAvatarVcard(account, avatar, callback);
2759						break;
2760				}
2761			}
2762		}
2763	}
2764
2765	private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2766		IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
2767		sendIqPacket(account, packet, new OnIqPacketReceived() {
2768
2769			@Override
2770			public void onIqPacketReceived(Account account, IqPacket result) {
2771				synchronized (mInProgressAvatarFetches) {
2772					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2773				}
2774				final String ERROR = account.getJid().toBareJid()
2775						+ ": fetching avatar for " + avatar.owner + " failed ";
2776				if (result.getType() == IqPacket.TYPE.RESULT) {
2777					avatar.image = mIqParser.avatarData(result);
2778					if (avatar.image != null) {
2779						if (getFileBackend().save(avatar)) {
2780							if (account.getJid().toBareJid().equals(avatar.owner)) {
2781								if (account.setAvatar(avatar.getFilename())) {
2782									databaseBackend.updateAccount(account);
2783								}
2784								getAvatarService().clear(account);
2785								updateConversationUi();
2786								updateAccountUi();
2787							} else {
2788								Contact contact = account.getRoster()
2789										.getContact(avatar.owner);
2790								contact.setAvatar(avatar);
2791								getAvatarService().clear(contact);
2792								updateConversationUi();
2793								updateRosterUi();
2794							}
2795							if (callback != null) {
2796								callback.success(avatar);
2797							}
2798							Log.d(Config.LOGTAG, account.getJid().toBareJid()
2799									+ ": successfully fetched pep avatar for " + avatar.owner);
2800							return;
2801						}
2802					} else {
2803
2804						Log.d(Config.LOGTAG, ERROR + "(parsing error)");
2805					}
2806				} else {
2807					Element error = result.findChild("error");
2808					if (error == null) {
2809						Log.d(Config.LOGTAG, ERROR + "(server error)");
2810					} else {
2811						Log.d(Config.LOGTAG, ERROR + error.toString());
2812					}
2813				}
2814				if (callback != null) {
2815					callback.error(0, null);
2816				}
2817
2818			}
2819		});
2820	}
2821
2822	private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2823		IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
2824		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2825			@Override
2826			public void onIqPacketReceived(Account account, IqPacket packet) {
2827				synchronized (mInProgressAvatarFetches) {
2828					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2829				}
2830				if (packet.getType() == IqPacket.TYPE.RESULT) {
2831					Element vCard = packet.findChild("vCard", "vcard-temp");
2832					Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
2833					String image = photo != null ? photo.findChildContent("BINVAL") : null;
2834					if (image != null) {
2835						avatar.image = image;
2836						if (getFileBackend().save(avatar)) {
2837							Log.d(Config.LOGTAG, account.getJid().toBareJid()
2838									+ ": successfully fetched vCard avatar for " + avatar.owner);
2839							if (avatar.owner.isBareJid()) {
2840								if (account.getJid().toBareJid().equals(avatar.owner) && account.getAvatar() == null) {
2841									Log.d(Config.LOGTAG,account.getJid().toBareJid()+": had no avatar. replacing with vcard");
2842									account.setAvatar(avatar.getFilename());
2843									databaseBackend.updateAccount(account);
2844									getAvatarService().clear(account);
2845									updateAccountUi();
2846								} else {
2847									Contact contact = account.getRoster().getContact(avatar.owner);
2848									contact.setAvatar(avatar);
2849									getAvatarService().clear(contact);
2850									updateRosterUi();
2851								}
2852								updateConversationUi();
2853							} else {
2854								Conversation conversation = find(account, avatar.owner.toBareJid());
2855								if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
2856									MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner);
2857									if (user != null) {
2858										if (user.setAvatar(avatar)) {
2859											getAvatarService().clear(user);
2860											updateConversationUi();
2861											updateMucRosterUi();
2862										}
2863									}
2864								}
2865							}
2866						}
2867					}
2868				}
2869			}
2870		});
2871	}
2872
2873	public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
2874		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
2875		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2876
2877			@Override
2878			public void onIqPacketReceived(Account account, IqPacket packet) {
2879				if (packet.getType() == IqPacket.TYPE.RESULT) {
2880					Element pubsub = packet.findChild("pubsub","http://jabber.org/protocol/pubsub");
2881					if (pubsub != null) {
2882						Element items = pubsub.findChild("items");
2883						if (items != null) {
2884							Avatar avatar = Avatar.parseMetadata(items);
2885							if (avatar != null) {
2886								avatar.owner = account.getJid().toBareJid();
2887								if (fileBackend.isAvatarCached(avatar)) {
2888									if (account.setAvatar(avatar.getFilename())) {
2889										databaseBackend.updateAccount(account);
2890									}
2891									getAvatarService().clear(account);
2892									callback.success(avatar);
2893								} else {
2894									fetchAvatarPep(account, avatar, callback);
2895								}
2896								return;
2897							}
2898						}
2899					}
2900				}
2901				callback.error(0, null);
2902			}
2903		});
2904	}
2905
2906	public void deleteContactOnServer(Contact contact) {
2907		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
2908		contact.resetOption(Contact.Options.DIRTY_PUSH);
2909		contact.setOption(Contact.Options.DIRTY_DELETE);
2910		Account account = contact.getAccount();
2911		if (account.getStatus() == Account.State.ONLINE) {
2912			IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2913			Element item = iq.query(Xmlns.ROSTER).addChild("item");
2914			item.setAttribute("jid", contact.getJid().toString());
2915			item.setAttribute("subscription", "remove");
2916			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2917		}
2918	}
2919
2920	public void updateConversation(final Conversation conversation) {
2921		mDatabaseExecutor.execute(new Runnable() {
2922			@Override
2923			public void run() {
2924				databaseBackend.updateConversation(conversation);
2925			}
2926		});
2927	}
2928
2929	private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
2930		synchronized (account) {
2931			XmppConnection connection = account.getXmppConnection();
2932			if (connection == null) {
2933				connection = createConnection(account);
2934				account.setXmppConnection(connection);
2935			}
2936			boolean hasInternet = hasInternetConnection();
2937			if (!account.isOptionSet(Account.OPTION_DISABLED) && hasInternet) {
2938				if (!force) {
2939					disconnect(account, false);
2940				}
2941				Thread thread = new Thread(connection);
2942				connection.setInteractive(interactive);
2943				connection.prepareNewConnection();
2944				connection.interrupt();
2945				thread.start();
2946				scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
2947			} else {
2948				disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
2949				account.getRoster().clearPresences();
2950				connection.resetEverything();
2951				account.getAxolotlService().resetBrokenness();
2952				if (!hasInternet) {
2953					account.setStatus(Account.State.NO_INTERNET);
2954				}
2955			}
2956		}
2957	}
2958
2959	public void reconnectAccountInBackground(final Account account) {
2960		new Thread(new Runnable() {
2961			@Override
2962			public void run() {
2963				reconnectAccount(account, false, true);
2964			}
2965		}).start();
2966	}
2967
2968	public void invite(Conversation conversation, Jid contact) {
2969		Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": inviting " + contact + " to " + conversation.getJid().toBareJid());
2970		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
2971		sendMessagePacket(conversation.getAccount(), packet);
2972	}
2973
2974	public void directInvite(Conversation conversation, Jid jid) {
2975		MessagePacket packet = mMessageGenerator.directInvite(conversation, jid);
2976		sendMessagePacket(conversation.getAccount(), packet);
2977	}
2978
2979	public void resetSendingToWaiting(Account account) {
2980		for (Conversation conversation : getConversations()) {
2981			if (conversation.getAccount() == account) {
2982				conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
2983
2984					@Override
2985					public void onMessageFound(Message message) {
2986						markMessage(message, Message.STATUS_WAITING);
2987					}
2988				});
2989			}
2990		}
2991	}
2992
2993	public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
2994		return markMessage(account, recipient, uuid, status, null);
2995	}
2996
2997	public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status, String errorMessage) {
2998		if (uuid == null) {
2999			return null;
3000		}
3001		for (Conversation conversation : getConversations()) {
3002			if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) {
3003				final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
3004				if (message != null) {
3005					markMessage(message, status, errorMessage);
3006				}
3007				return message;
3008			}
3009		}
3010		return null;
3011	}
3012
3013	public boolean markMessage(Conversation conversation, String uuid, int status) {
3014		if (uuid == null) {
3015			return false;
3016		} else {
3017			Message message = conversation.findSentMessageWithUuid(uuid);
3018			if (message != null) {
3019				markMessage(message, status);
3020				return true;
3021			} else {
3022				return false;
3023			}
3024		}
3025	}
3026
3027	public void markMessage(Message message, int status) {
3028		markMessage(message, status, null);
3029	}
3030
3031
3032	public void markMessage(Message message, int status, String errorMessage) {
3033		if (status == Message.STATUS_SEND_FAILED
3034				&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
3035				.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
3036			return;
3037		}
3038		message.setErrorMessage(errorMessage);
3039		message.setStatus(status);
3040		databaseBackend.updateMessage(message);
3041		updateConversationUi();
3042	}
3043
3044	public SharedPreferences getPreferences() {
3045		return PreferenceManager
3046				.getDefaultSharedPreferences(getApplicationContext());
3047	}
3048
3049	public boolean confirmMessages() {
3050		return getPreferences().getBoolean("confirm_messages", true);
3051	}
3052
3053	public boolean allowMessageCorrection() {
3054		return getPreferences().getBoolean("allow_message_correction", true);
3055	}
3056
3057	public boolean sendChatStates() {
3058		return getPreferences().getBoolean("chat_states", false);
3059	}
3060
3061	public boolean saveEncryptedMessages() {
3062		return !getPreferences().getBoolean("dont_save_encrypted", false);
3063	}
3064
3065	private boolean respectAutojoin() {
3066		return getPreferences().getBoolean("autojoin", true);
3067	}
3068
3069	public boolean indicateReceived() {
3070		return getPreferences().getBoolean("indicate_received", false);
3071	}
3072
3073	public boolean useTorToConnect() {
3074		return Config.FORCE_ORBOT || getPreferences().getBoolean("use_tor", false);
3075	}
3076
3077	public boolean showExtendedConnectionOptions() {
3078		return getPreferences().getBoolean("show_connection_options", false);
3079	}
3080
3081	public boolean broadcastLastActivity() {
3082		return getPreferences().getBoolean("last_activity", false);
3083	}
3084
3085	public int unreadCount() {
3086		int count = 0;
3087		for (Conversation conversation : getConversations()) {
3088			count += conversation.unreadCount();
3089		}
3090		return count;
3091	}
3092
3093
3094	public void showErrorToastInUi(int resId) {
3095		if (mOnShowErrorToast != null) {
3096			mOnShowErrorToast.onShowErrorToast(resId);
3097		}
3098	}
3099
3100	public void updateConversationUi() {
3101		if (mOnConversationUpdate != null) {
3102			mOnConversationUpdate.onConversationUpdate();
3103		}
3104	}
3105
3106	public void updateAccountUi() {
3107		if (mOnAccountUpdate != null) {
3108			mOnAccountUpdate.onAccountUpdate();
3109		}
3110	}
3111
3112	public void updateRosterUi() {
3113		if (mOnRosterUpdate != null) {
3114			mOnRosterUpdate.onRosterUpdate();
3115		}
3116	}
3117
3118	public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
3119		if (mOnCaptchaRequested != null) {
3120			DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
3121			Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity),
3122					(int) (captcha.getHeight() * metrics.scaledDensity), false);
3123
3124			mOnCaptchaRequested.onCaptchaRequested(account, id, data, scaled);
3125			return true;
3126		}
3127		return false;
3128	}
3129
3130	public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
3131		if (mOnUpdateBlocklist != null) {
3132			mOnUpdateBlocklist.OnUpdateBlocklist(status);
3133		}
3134	}
3135
3136	public void updateMucRosterUi() {
3137		if (mOnMucRosterUpdate != null) {
3138			mOnMucRosterUpdate.onMucRosterUpdate();
3139		}
3140	}
3141
3142	public void keyStatusUpdated(AxolotlService.FetchStatus report) {
3143		if (mOnKeyStatusUpdated != null) {
3144			mOnKeyStatusUpdated.onKeyStatusUpdated(report);
3145		}
3146	}
3147
3148	public Account findAccountByJid(final Jid accountJid) {
3149		for (Account account : this.accounts) {
3150			if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
3151				return account;
3152			}
3153		}
3154		return null;
3155	}
3156
3157	public Conversation findConversationByUuid(String uuid) {
3158		for (Conversation conversation : getConversations()) {
3159			if (conversation.getUuid().equals(uuid)) {
3160				return conversation;
3161			}
3162		}
3163		return null;
3164	}
3165
3166	public boolean markRead(final Conversation conversation) {
3167		return markRead(conversation,true);
3168	}
3169
3170	public boolean markRead(final Conversation conversation, boolean clear) {
3171		if (clear) {
3172			mNotificationService.clear(conversation);
3173		}
3174		final List<Message> readMessages = conversation.markRead();
3175		if (readMessages.size() > 0) {
3176			Runnable runnable = new Runnable() {
3177				@Override
3178				public void run() {
3179					for (Message message : readMessages) {
3180						databaseBackend.updateMessage(message);
3181					}
3182				}
3183			};
3184			mDatabaseExecutor.execute(runnable);
3185			updateUnreadCountBadge();
3186			return true;
3187		} else {
3188			return false;
3189		}
3190	}
3191
3192	public synchronized void updateUnreadCountBadge() {
3193		int count = unreadCount();
3194		if (unreadCount != count) {
3195			Log.d(Config.LOGTAG, "update unread count to " + count);
3196			if (count > 0) {
3197				ShortcutBadger.applyCount(getApplicationContext(), count);
3198			} else {
3199				ShortcutBadger.removeCount(getApplicationContext());
3200			}
3201			unreadCount = count;
3202		}
3203	}
3204
3205	public void sendReadMarker(final Conversation conversation) {
3206		final Message markable = conversation.getLatestMarkableMessage();
3207		if (this.markRead(conversation)) {
3208			updateConversationUi();
3209		}
3210		if (confirmMessages()
3211				&& markable != null
3212				&& markable.trusted()
3213				&& markable.getRemoteMsgId() != null) {
3214			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
3215			Account account = conversation.getAccount();
3216			final Jid to = markable.getCounterpart();
3217			MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
3218			this.sendMessagePacket(conversation.getAccount(), packet);
3219		}
3220	}
3221
3222	public SecureRandom getRNG() {
3223		return this.mRandom;
3224	}
3225
3226	public MemorizingTrustManager getMemorizingTrustManager() {
3227		return this.mMemorizingTrustManager;
3228	}
3229
3230	public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
3231		this.mMemorizingTrustManager = trustManager;
3232	}
3233
3234	public void updateMemorizingTrustmanager() {
3235		final MemorizingTrustManager tm;
3236		final boolean dontTrustSystemCAs = getPreferences().getBoolean("dont_trust_system_cas", false);
3237		if (dontTrustSystemCAs) {
3238			tm = new MemorizingTrustManager(getApplicationContext(), null);
3239		} else {
3240			tm = new MemorizingTrustManager(getApplicationContext());
3241		}
3242		setMemorizingTrustManager(tm);
3243	}
3244
3245	public PowerManager getPowerManager() {
3246		return this.pm;
3247	}
3248
3249	public LruCache<String, Bitmap> getBitmapCache() {
3250		return this.mBitmapCache;
3251	}
3252
3253	public void syncRosterToDisk(final Account account) {
3254		Runnable runnable = new Runnable() {
3255
3256			@Override
3257			public void run() {
3258				databaseBackend.writeRoster(account.getRoster());
3259			}
3260		};
3261		mDatabaseExecutor.execute(runnable);
3262
3263	}
3264
3265	public List<String> getKnownHosts() {
3266		final List<String> hosts = new ArrayList<>();
3267		for (final Account account : getAccounts()) {
3268			if (!hosts.contains(account.getServer().toString())) {
3269				hosts.add(account.getServer().toString());
3270			}
3271			for (final Contact contact : account.getRoster().getContacts()) {
3272				if (contact.showInRoster()) {
3273					final String server = contact.getServer().toString();
3274					if (server != null && !hosts.contains(server)) {
3275						hosts.add(server);
3276					}
3277				}
3278			}
3279		}
3280		if(Config.DOMAIN_LOCK != null && !hosts.contains(Config.DOMAIN_LOCK)) {
3281			hosts.add(Config.DOMAIN_LOCK);
3282		}
3283		if(Config.MAGIC_CREATE_DOMAIN != null && !hosts.contains(Config.MAGIC_CREATE_DOMAIN)) {
3284			hosts.add(Config.MAGIC_CREATE_DOMAIN);
3285		}
3286		return hosts;
3287	}
3288
3289	public List<String> getKnownConferenceHosts() {
3290		final ArrayList<String> mucServers = new ArrayList<>();
3291		for (final Account account : accounts) {
3292			if (account.getXmppConnection() != null) {
3293				final String server = account.getXmppConnection().getMucServer();
3294				if (server != null && !mucServers.contains(server)) {
3295					mucServers.add(server);
3296				}
3297			}
3298		}
3299		return mucServers;
3300	}
3301
3302	public void sendMessagePacket(Account account, MessagePacket packet) {
3303		XmppConnection connection = account.getXmppConnection();
3304		if (connection != null) {
3305			connection.sendMessagePacket(packet);
3306		}
3307	}
3308
3309	public void sendPresencePacket(Account account, PresencePacket packet) {
3310		XmppConnection connection = account.getXmppConnection();
3311		if (connection != null) {
3312			connection.sendPresencePacket(packet);
3313		}
3314	}
3315
3316	public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
3317		final XmppConnection connection = account.getXmppConnection();
3318		if (connection != null) {
3319			IqPacket request = mIqGenerator.generateCreateAccountWithCaptcha(account, id, data);
3320			connection.sendUnmodifiedIqPacket(request, connection.registrationResponseListener);
3321		}
3322	}
3323
3324	public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
3325		final XmppConnection connection = account.getXmppConnection();
3326		if (connection != null) {
3327			connection.sendIqPacket(packet, callback);
3328		}
3329	}
3330
3331	public void sendPresence(final Account account) {
3332		sendPresence(account, checkListeners() && broadcastLastActivity());
3333	}
3334
3335	private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
3336		PresencePacket packet;
3337		if (manuallyChangePresence()) {
3338			packet =  mPresenceGenerator.selfPresence(account, account.getPresenceStatus());
3339			String message = account.getPresenceStatusMessage();
3340			if (message != null && !message.isEmpty()) {
3341				packet.addChild(new Element("status").setContent(message));
3342			}
3343		} else {
3344			packet = mPresenceGenerator.selfPresence(account, getTargetPresence());
3345		}
3346		if (mLastActivity > 0 && includeIdleTimestamp) {
3347			long since = Math.min(mLastActivity, System.currentTimeMillis()); //don't send future dates
3348			packet.addChild("idle","urn:xmpp:idle:1").setAttribute("since", AbstractGenerator.getTimestamp(since));
3349		}
3350		sendPresencePacket(account, packet);
3351	}
3352
3353	private void deactivateGracePeriod() {
3354		for(Account account : getAccounts()) {
3355			account.deactivateGracePeriod();
3356		}
3357	}
3358
3359	public void refreshAllPresences() {
3360		boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
3361		for (Account account : getAccounts()) {
3362			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
3363				sendPresence(account, includeIdleTimestamp);
3364			}
3365		}
3366	}
3367
3368	private void refreshAllGcmTokens() {
3369		for(Account account : getAccounts()) {
3370			if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
3371				mPushManagementService.registerPushTokenOnServer(account);
3372			}
3373		}
3374	}
3375
3376	private void sendOfflinePresence(final Account account) {
3377		Log.d(Config.LOGTAG,account.getJid().toBareJid()+": sending offline presence");
3378		sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
3379	}
3380
3381	public MessageGenerator getMessageGenerator() {
3382		return this.mMessageGenerator;
3383	}
3384
3385	public PresenceGenerator getPresenceGenerator() {
3386		return this.mPresenceGenerator;
3387	}
3388
3389	public IqGenerator getIqGenerator() {
3390		return this.mIqGenerator;
3391	}
3392
3393	public IqParser getIqParser() {
3394		return this.mIqParser;
3395	}
3396
3397	public JingleConnectionManager getJingleConnectionManager() {
3398		return this.mJingleConnectionManager;
3399	}
3400
3401	public MessageArchiveService getMessageArchiveService() {
3402		return this.mMessageArchiveService;
3403	}
3404
3405	public List<Contact> findContacts(Jid jid) {
3406		ArrayList<Contact> contacts = new ArrayList<>();
3407		for (Account account : getAccounts()) {
3408			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
3409				Contact contact = account.getRoster().getContactFromRoster(jid);
3410				if (contact != null) {
3411					contacts.add(contact);
3412				}
3413			}
3414		}
3415		return contacts;
3416	}
3417
3418	public Conversation findFirstMuc(Jid jid) {
3419		for(Conversation conversation : getConversations()) {
3420			if (conversation.getJid().toBareJid().equals(jid.toBareJid())
3421					&& conversation.getMode() == Conversation.MODE_MULTI) {
3422				return conversation;
3423			}
3424		}
3425		return null;
3426	}
3427
3428	public NotificationService getNotificationService() {
3429		return this.mNotificationService;
3430	}
3431
3432	public HttpConnectionManager getHttpConnectionManager() {
3433		return this.mHttpConnectionManager;
3434	}
3435
3436	public void resendFailedMessages(final Message message) {
3437		final Collection<Message> messages = new ArrayList<>();
3438		Message current = message;
3439		while (current.getStatus() == Message.STATUS_SEND_FAILED) {
3440			messages.add(current);
3441			if (current.mergeable(current.next())) {
3442				current = current.next();
3443			} else {
3444				break;
3445			}
3446		}
3447		for (final Message msg : messages) {
3448			msg.setTime(System.currentTimeMillis());
3449			markMessage(msg, Message.STATUS_WAITING);
3450			this.resendMessage(msg, false);
3451		}
3452	}
3453
3454	public void clearConversationHistory(final Conversation conversation) {
3455		conversation.clearMessages();
3456		conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
3457		conversation.setLastClearHistory(System.currentTimeMillis());
3458		Runnable runnable = new Runnable() {
3459			@Override
3460			public void run() {
3461				databaseBackend.deleteMessagesInConversation(conversation);
3462				databaseBackend.updateConversation(conversation);
3463			}
3464		};
3465		mDatabaseExecutor.execute(runnable);
3466	}
3467
3468	public void sendBlockRequest(final Blockable blockable, boolean reportSpam) {
3469		if (blockable != null && blockable.getBlockedJid() != null) {
3470			final Jid jid = blockable.getBlockedJid();
3471			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam), new OnIqPacketReceived() {
3472
3473				@Override
3474				public void onIqPacketReceived(final Account account, final IqPacket packet) {
3475					if (packet.getType() == IqPacket.TYPE.RESULT) {
3476						account.getBlocklist().add(jid);
3477						updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
3478					}
3479				}
3480			});
3481		}
3482	}
3483
3484	public void sendUnblockRequest(final Blockable blockable) {
3485		if (blockable != null && blockable.getJid() != null) {
3486			final Jid jid = blockable.getBlockedJid();
3487			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
3488				@Override
3489				public void onIqPacketReceived(final Account account, final IqPacket packet) {
3490					if (packet.getType() == IqPacket.TYPE.RESULT) {
3491						account.getBlocklist().remove(jid);
3492						updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
3493					}
3494				}
3495			});
3496		}
3497	}
3498
3499	public void publishDisplayName(Account account) {
3500		String displayName = account.getDisplayName();
3501		if (displayName != null && !displayName.isEmpty()) {
3502			IqPacket publish = mIqGenerator.publishNick(displayName);
3503			sendIqPacket(account, publish, new OnIqPacketReceived() {
3504				@Override
3505				public void onIqPacketReceived(Account account, IqPacket packet) {
3506					if (packet.getType() == IqPacket.TYPE.ERROR) {
3507						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not publish nick");
3508					}
3509				}
3510			});
3511		}
3512	}
3513
3514	public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
3515		ServiceDiscoveryResult result = discoCache.get(key);
3516		if (result != null) {
3517			return result;
3518		} else {
3519			result = databaseBackend.findDiscoveryResult(key.first, key.second);
3520			if (result != null) {
3521				discoCache.put(key, result);
3522			}
3523			return result;
3524		}
3525	}
3526
3527	public void fetchCaps(Account account, final Jid jid, final Presence presence) {
3528		final Pair<String,String> key = new Pair<>(presence.getHash(), presence.getVer());
3529		ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
3530		if (disco != null) {
3531			presence.setServiceDiscoveryResult(disco);
3532		} else {
3533			if (!account.inProgressDiscoFetches.contains(key)) {
3534				account.inProgressDiscoFetches.add(key);
3535				IqPacket request = new IqPacket(IqPacket.TYPE.GET);
3536				request.setTo(jid);
3537				request.query("http://jabber.org/protocol/disco#info");
3538				Log.d(Config.LOGTAG,account.getJid().toBareJid()+": making disco request for "+key.second+" to "+jid);
3539				sendIqPacket(account, request, new OnIqPacketReceived() {
3540					@Override
3541					public void onIqPacketReceived(Account account, IqPacket discoPacket) {
3542						if (discoPacket.getType() == IqPacket.TYPE.RESULT) {
3543							ServiceDiscoveryResult disco = new ServiceDiscoveryResult(discoPacket);
3544							if (presence.getVer().equals(disco.getVer())) {
3545								databaseBackend.insertDiscoveryResult(disco);
3546								injectServiceDiscorveryResult(account.getRoster(), presence.getHash(), presence.getVer(), disco);
3547							} else {
3548								Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + disco.getVer());
3549							}
3550						}
3551						account.inProgressDiscoFetches.remove(key);
3552					}
3553				});
3554			}
3555		}
3556	}
3557
3558	private void injectServiceDiscorveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) {
3559		for(Contact contact : roster.getContacts()) {
3560			for(Presence presence : contact.getPresences().getPresences().values()) {
3561				if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
3562					presence.setServiceDiscoveryResult(disco);
3563				}
3564			}
3565		}
3566	}
3567
3568	public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) {
3569		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
3570		request.addChild("prefs","urn:xmpp:mam:0");
3571		sendIqPacket(account, request, new OnIqPacketReceived() {
3572			@Override
3573			public void onIqPacketReceived(Account account, IqPacket packet) {
3574				Element prefs = packet.findChild("prefs","urn:xmpp:mam:0");
3575				if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) {
3576					callback.onPreferencesFetched(prefs);
3577				} else {
3578					callback.onPreferencesFetchFailed();
3579				}
3580			}
3581		});
3582	}
3583
3584	public PushManagementService getPushManagementService() {
3585		return mPushManagementService;
3586	}
3587
3588	public Account getPendingAccount() {
3589		Account pending = null;
3590		for(Account account : getAccounts()) {
3591			if (account.isOptionSet(Account.OPTION_REGISTER)) {
3592				pending = account;
3593			} else {
3594				return null;
3595			}
3596		}
3597		return pending;
3598	}
3599
3600	public void changeStatus(Account account, Presence.Status status, String statusMessage, boolean send) {
3601		if (!statusMessage.isEmpty()) {
3602			databaseBackend.insertPresenceTemplate(new PresenceTemplate(status, statusMessage));
3603		}
3604		changeStatusReal(account, status, statusMessage, send);
3605	}
3606
3607	private void changeStatusReal(Account account, Presence.Status status, String statusMessage, boolean send) {
3608		account.setPresenceStatus(status);
3609		account.setPresenceStatusMessage(statusMessage);
3610		databaseBackend.updateAccount(account);
3611		if (!account.isOptionSet(Account.OPTION_DISABLED) && send) {
3612			sendPresence(account);
3613		}
3614	}
3615
3616	public void changeStatus(Presence.Status status, String statusMessage) {
3617		if (!statusMessage.isEmpty()) {
3618			databaseBackend.insertPresenceTemplate(new PresenceTemplate(status, statusMessage));
3619		}
3620		for(Account account : getAccounts()) {
3621			changeStatusReal(account, status, statusMessage, true);
3622		}
3623	}
3624
3625	public List<PresenceTemplate> getPresenceTemplates(Account account) {
3626		List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
3627		for(PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
3628			if (!templates.contains(template)) {
3629				templates.add(0, template);
3630			}
3631		}
3632		return templates;
3633	}
3634
3635	public void saveConversationAsBookmark(Conversation conversation, String name) {
3636		Account account = conversation.getAccount();
3637		Bookmark bookmark = new Bookmark(account, conversation.getJid().toBareJid());
3638		if (!conversation.getJid().isBareJid()) {
3639			bookmark.setNick(conversation.getJid().getResourcepart());
3640		}
3641		if (name != null && !name.trim().isEmpty()) {
3642			bookmark.setBookmarkName(name.trim());
3643		}
3644		bookmark.setAutojoin(getPreferences().getBoolean("autojoin",true));
3645		account.getBookmarks().add(bookmark);
3646		pushBookmarks(account);
3647		conversation.setBookmark(bookmark);
3648	}
3649
3650	public void clearStartTimeCounter() {
3651		mDatabaseExecutor.execute(new Runnable() {
3652			@Override
3653			public void run() {
3654				databaseBackend.clearStartTimeCounter(false);
3655			}
3656		});
3657	}
3658
3659	public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
3660		boolean needsRosterWrite = false;
3661		boolean performedVerification = false;
3662		final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
3663		for(XmppUri.Fingerprint fp : fingerprints) {
3664			if (fp.type == XmppUri.FingerprintType.OTR) {
3665				performedVerification |= contact.addOtrFingerprint(fp.fingerprint);
3666				needsRosterWrite |= performedVerification;
3667			} else if (fp.type == XmppUri.FingerprintType.OMEMO) {
3668				String fingerprint = "05"+fp.fingerprint.replaceAll("\\s","");
3669				FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
3670				if (fingerprintStatus != null) {
3671					if (!fingerprintStatus.isVerified()) {
3672						performedVerification = true;
3673						axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified());
3674					}
3675				} else {
3676					axolotlService.preVerifyFingerprint(contact,fingerprint);
3677				}
3678			}
3679		}
3680		if (needsRosterWrite) {
3681			syncRosterToDisk(contact.getAccount());
3682		}
3683		return performedVerification;
3684	}
3685
3686	public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
3687		final AxolotlService axolotlService = account.getAxolotlService();
3688		boolean verifiedSomething = false;
3689		for(XmppUri.Fingerprint fp : fingerprints) {
3690			if (fp.type == XmppUri.FingerprintType.OMEMO) {
3691				String fingerprint = "05"+fp.fingerprint.replaceAll("\\s","");
3692				Log.d(Config.LOGTAG,"trying to verify own fp="+fingerprint);
3693				FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
3694				if (fingerprintStatus != null) {
3695					if (!fingerprintStatus.isVerified()) {
3696						axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified());
3697						verifiedSomething = true;
3698					}
3699				} else {
3700					axolotlService.preVerifyFingerprint(account,fingerprint);
3701					verifiedSomething = true;
3702				}
3703			}
3704		}
3705		return verifiedSomething;
3706	}
3707
3708	public boolean blindTrustBeforeVerification() {
3709		return getPreferences().getBoolean(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, true);
3710	}
3711
3712	public interface OnMamPreferencesFetched {
3713		void onPreferencesFetched(Element prefs);
3714		void onPreferencesFetchFailed();
3715	}
3716
3717	public void pushMamPreferences(Account account, Element prefs) {
3718		IqPacket set = new IqPacket(IqPacket.TYPE.SET);
3719		set.addChild(prefs);
3720		sendIqPacket(account, set, null);
3721	}
3722
3723	public interface OnAccountCreated {
3724		void onAccountCreated(Account account);
3725
3726		void informUser(int r);
3727	}
3728
3729	public interface OnMoreMessagesLoaded {
3730		void onMoreMessagesLoaded(int count, Conversation conversation);
3731
3732		void informUser(int r);
3733	}
3734
3735	public interface OnAccountPasswordChanged {
3736		void onPasswordChangeSucceeded();
3737
3738		void onPasswordChangeFailed();
3739	}
3740
3741	public interface OnAffiliationChanged {
3742		void onAffiliationChangedSuccessful(Jid jid);
3743
3744		void onAffiliationChangeFailed(Jid jid, int resId);
3745	}
3746
3747	public interface OnRoleChanged {
3748		void onRoleChangedSuccessful(String nick);
3749
3750		void onRoleChangeFailed(String nick, int resid);
3751	}
3752
3753	public interface OnConversationUpdate {
3754		void onConversationUpdate();
3755	}
3756
3757	public interface OnAccountUpdate {
3758		void onAccountUpdate();
3759	}
3760
3761	public interface OnCaptchaRequested {
3762		void onCaptchaRequested(Account account,
3763								String id,
3764								Data data,
3765								Bitmap captcha);
3766	}
3767
3768	public interface OnRosterUpdate {
3769		void onRosterUpdate();
3770	}
3771
3772	public interface OnMucRosterUpdate {
3773		void onMucRosterUpdate();
3774	}
3775
3776	public interface OnConferenceConfigurationFetched {
3777		void onConferenceConfigurationFetched(Conversation conversation);
3778
3779		void onFetchFailed(Conversation conversation, Element error);
3780	}
3781
3782	public interface OnConferenceJoined {
3783		void onConferenceJoined(Conversation conversation);
3784	}
3785
3786	public interface OnConferenceOptionsPushed {
3787		void onPushSucceeded();
3788
3789		void onPushFailed();
3790	}
3791
3792	public interface OnShowErrorToast {
3793		void onShowErrorToast(int resId);
3794	}
3795
3796	public class XmppConnectionBinder extends Binder {
3797		public XmppConnectionService getService() {
3798			return XmppConnectionService.this;
3799		}
3800	}
3801}