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