XmppConnectionService.java

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