XmppConnectionService.java

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