XmppConnectionService.java

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