XmppConnectionService.java

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