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