XmppConnectionService.java

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