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