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